@aikidosec/firewall
Version:
Zen by Aikido is an embedded Application Firewall that autonomously protects Node.js apps against common and critical attacks, provides rate limiting, detects malicious traffic (including bots), and more.
250 lines (249 loc) • 11.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MySQL2 = void 0;
const Context_1 = require("../agent/Context");
const wrapExport_1 = require("../agent/hooks/wrapExport");
const wrap_1 = require("../helpers/wrap");
const checkContextForSqlInjection_1 = require("../vulnerabilities/sql-injection/checkContextForSqlInjection");
const checkContextForIdor_1 = require("../vulnerabilities/idor/checkContextForIdor");
const SQLDialectMySQL_1 = require("../vulnerabilities/sql-injection/dialects/SQLDialectMySQL");
const colorText_1 = require("../helpers/colorText");
const getPackageVersion_1 = require("../helpers/getPackageVersion");
const isVersionGreaterOrEqual_1 = require("../helpers/isVersionGreaterOrEqual");
const warnBox_1 = require("../helpers/warnBox");
class MySQL2 {
constructor() {
this.dialect = new SQLDialectMySQL_1.SQLDialectMySQL();
}
resolvePlaceholder(placeholder, placeholderNumber, params) {
if (placeholder === "?" && placeholderNumber !== undefined && params) {
if (placeholderNumber < params.length) {
return params[placeholderNumber];
}
}
return undefined;
}
findParams(args) {
if (args.length >= 2 && Array.isArray(args[1])) {
return args[1];
}
return undefined;
}
getSQLStringFromArgs(args) {
if (args.length <= 0) {
return { sql: undefined, params: undefined };
}
if (typeof args[0] === "string" && args[0].length > 0) {
const sql = args[0];
return {
sql,
params: this.findParams(args),
};
}
// Do not use isPlainObject here, since mysql2 Pools pass complex objects
if (args[0] &&
typeof args[0] === "object" &&
!Array.isArray(args[0]) &&
"sql" in args[0] &&
typeof args[0].sql === "string") {
const sql = args[0].sql;
return {
sql,
params: this.findParams(args),
};
}
return { sql: undefined, params: undefined };
}
inspectQuery(operation, args) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
const { sql, params } = this.getSQLStringFromArgs(args);
if (!sql) {
return undefined;
}
// Check for SQL injection first to block malicious queries before parsing SQL query for IDOR analysis
const sqlInjectionResult = (0, checkContextForSqlInjection_1.checkContextForSqlInjection)({
operation: operation,
sql: sql,
context: context,
dialect: this.dialect,
});
if (sqlInjectionResult) {
return sqlInjectionResult;
}
return (0, checkContextForIdor_1.checkContextForIdor)({
sql,
context,
dialect: this.dialect,
resolvePlaceholder: (placeholder, placeholderNumber) => this.resolvePlaceholder(placeholder, placeholderNumber, params),
});
}
// This function is copied from the OpenTelemetry MySQL2 instrumentation (Apache 2.0 license)
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/21e1331a29e06092fb1e460ca99e0c28b1b57ac4/plugins/node/opentelemetry-instrumentation-mysql2/src/utils.ts#L150
getPrototypeToInstrument(connection) {
const connectionPrototype = connection.prototype;
const basePrototype = Object.getPrototypeOf(connectionPrototype);
// mysql2@3.11.5 included a refactoring, where most code was moved out of the `Connection` class and into a shared base
// so we need to instrument that instead, see https://github.com/sidorares/node-mysql2/pull/3081
// This checks if the functions we're instrumenting are there on the base - we cannot use the presence of a base
// prototype since EventEmitter is the base for mysql2@<=3.11.4
if (typeof (basePrototype === null || basePrototype === void 0 ? void 0 : basePrototype.query) === "function" &&
typeof (basePrototype === null || basePrototype === void 0 ? void 0 : basePrototype.execute) === "function") {
return basePrototype;
}
// otherwise instrument the connection directly.
return connectionPrototype;
}
getConnectionFunctionInstructions() {
return [
{
nodeType: "MethodDefinition",
name: "query",
inspectArgs: (args) => this.inspectQuery("mysql2.query", args),
operationKind: "sql_op",
bindContext: true,
},
{
nodeType: "MethodDefinition",
name: "execute",
inspectArgs: (args) => this.inspectQuery("mysql2.execute", args),
operationKind: "sql_op",
bindContext: true,
},
{
nodeType: "MethodDefinition",
name: "prepare",
inspectArgs: (args) => this.inspectQuery("mysql2.prepare", args),
operationKind: "sql_op",
bindContext: true,
},
];
}
getPoolFunctionInstructions() {
return [
{
nodeType: "MethodDefinition",
name: "getConnection",
// This is required to bind the context, so that we do not loose context
// on pool operations like pool.query or pool.execute which internally call getConnection
// with a callback function
inspectArgs: () => { },
operationKind: "sql_op",
bindContext: true,
},
{
nodeType: "MethodDefinition",
name: "query",
inspectArgs: (args) => this.inspectQuery("mysql2.query", args),
operationKind: "sql_op",
bindContext: true,
},
{
nodeType: "MethodDefinition",
name: "execute",
inspectArgs: (args) => this.inspectQuery("mysql2.execute", args),
operationKind: "sql_op",
bindContext: true,
},
];
}
printOutdatedWarningIfNeeded(pkgName) {
const version = (0, getPackageVersion_1.getPackageVersion)(pkgName);
if (!version) {
return;
}
if (!(0, isVersionGreaterOrEqual_1.isVersionGreaterOrEqual)("3.11.5", version)) {
// oxlint-disable-next-line no-console
console.warn((0, colorText_1.colorText)("red", (0, warnBox_1.warnBox)("Zen can NOT protect your application. mysql2 versions older than 3.11.5 have circular dependencies in their internals that prevent Zen from instrumenting pools (https://github.com/sidorares/node-mysql2/pull/3081). Upgrade mysql2 to 3.11.5 or newer.")));
}
}
wrap(hooks) {
const wrapConnectionAndPool = (exports, pkgInfo, isPromise) => {
const connectionPrototype = this.getPrototypeToInstrument(isPromise ? exports.PromiseConnection : exports.Connection);
if (!(0, wrap_1.isWrapped)(connectionPrototype.query)) {
// Wrap connection.query
(0, wrapExport_1.wrapExport)(connectionPrototype, "query", pkgInfo, {
kind: "sql_op",
inspectArgs: (args) => this.inspectQuery("mysql2.query", args),
});
}
if (!(0, wrap_1.isWrapped)(connectionPrototype.execute)) {
// Wrap connection.execute
(0, wrapExport_1.wrapExport)(connectionPrototype, "execute", pkgInfo, {
kind: "sql_op",
inspectArgs: (args) => this.inspectQuery("mysql2.execute", args),
});
}
if (!(0, wrap_1.isWrapped)(connectionPrototype.prepare)) {
// Wrap connection.prepare
(0, wrapExport_1.wrapExport)(connectionPrototype, "prepare", pkgInfo, {
kind: "sql_op",
inspectArgs: (args) => this.inspectQuery("mysql2.prepare", args),
});
}
const poolPrototype = this.getPrototypeToInstrument(isPromise ? exports.PromisePool : exports.Pool);
if (!(0, wrap_1.isWrapped)(poolPrototype.getConnection)) {
// Wrap pool.getConnection
(0, wrapExport_1.wrapExport)(poolPrototype, "getConnection", pkgInfo, {
kind: "sql_op",
// This is required to bind the context, so that we do not loose context
// on pool operations like pool.query or pool.execute which internally call getConnection
// with a callback function
inspectArgs: () => { },
});
}
if (!(0, wrap_1.isWrapped)(poolPrototype.query)) {
// Wrap pool.query
(0, wrapExport_1.wrapExport)(poolPrototype, "query", pkgInfo, {
kind: "sql_op",
inspectArgs: (args) => this.inspectQuery("mysql2.query", args),
});
}
if (!(0, wrap_1.isWrapped)(poolPrototype.execute)) {
// Wrap pool.execute
(0, wrapExport_1.wrapExport)(poolPrototype, "execute", pkgInfo, {
kind: "sql_op",
inspectArgs: (args) => this.inspectQuery("mysql2.execute", args),
});
}
};
const pkg = hooks.addPackage("mysql2");
// For all versions of mysql2 newer than 3.0.0
pkg
.withVersion("^3.0.0")
.onRequire((exports, pkgInfo) => {
this.printOutdatedWarningIfNeeded(pkgInfo.name);
return wrapConnectionAndPool(exports, pkgInfo, false);
})
.addFileInstrumentation({
path: "lib/connection.js",
functions: this.getConnectionFunctionInstructions(),
accessLocalVariables: {
names: ["globalThis"], // Placeholder to run code on file load
cb: (_vars, pkgInfo) => this.printOutdatedWarningIfNeeded(pkgInfo.name),
},
})
.addFileInstrumentation({
path: "lib/pool.js",
functions: this.getPoolFunctionInstructions(),
});
// For all versions of mysql2 newer than / equal 3.11.5
// Reason: https://github.com/sidorares/node-mysql2/pull/3081
pkg
.withVersion("^3.11.5")
.onFileRequire("promise.js", (exports, pkgInfo) => {
return wrapConnectionAndPool(exports, pkgInfo, true);
})
.addFileInstrumentation({
path: "lib/base/connection.js",
functions: this.getConnectionFunctionInstructions(),
})
.addFileInstrumentation({
path: "lib/base/pool.js",
functions: this.getPoolFunctionInstructions(),
});
}
}
exports.MySQL2 = MySQL2;