@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.
218 lines (217 loc) • 8.84 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeSQLite = void 0;
const Context_1 = require("../agent/Context");
const wrapExport_1 = require("../agent/hooks/wrapExport");
const checkContextForSqlInjection_1 = require("../vulnerabilities/sql-injection/checkContextForSqlInjection");
const SQLDialectSQLite_1 = require("../vulnerabilities/sql-injection/dialects/SQLDialectSQLite");
const checkContextForIdor_1 = require("../vulnerabilities/idor/checkContextForIdor");
const isPlainObject_1 = require("../helpers/isPlainObject");
const zenRawQuerySymbol = Symbol("zen.node.sqlite.rawQuery");
class NodeSQLite {
constructor() {
this.dialect = new SQLDialectSQLite_1.SQLDialectSQLite();
}
wrap(hooks) {
const dbSqlFunctions = ["exec"];
const statementSqlFunctions = ["all", "get", "iterate", "run", "columns"];
const tagStoreSqlFunctions = ["all", "get", "iterate", "run"];
// Omit node: prefix because its an internal module
hooks.addBuiltinModule("sqlite").onRequire((exports, pkgInfo) => {
const dbSyncProto = exports.DatabaseSync.prototype;
for (const func of dbSqlFunctions) {
(0, wrapExport_1.wrapExport)(dbSyncProto, func, pkgInfo, {
kind: "sql_op",
inspectArgs: (args) => this.inspectQuery(`node:sqlite.${func}`, args),
});
}
(0, wrapExport_1.wrapExport)(dbSyncProto, "prepare", pkgInfo, {
kind: "sql_op",
modifyReturnValue: (args, returnValue) => this.addRawQueryToStatement(returnValue, args),
});
const statementProto = exports.StatementSync.prototype;
for (const func of statementSqlFunctions) {
(0, wrapExport_1.wrapExport)(statementProto, func, pkgInfo, {
kind: "sql_op",
inspectArgs: (args, _agent, subject) => this.inspectStatementQuery(`node:sqlite.StatementSync.${func}`, args, subject),
});
}
if (typeof dbSyncProto.createTagStore === "function") {
(0, wrapExport_1.wrapExport)(dbSyncProto, "createTagStore", pkgInfo, {
kind: "sql_op",
modifyReturnValue: (_args, returnValue) => {
for (const func of tagStoreSqlFunctions) {
(0, wrapExport_1.wrapExport)(returnValue, func, pkgInfo, {
kind: "sql_op",
inspectArgs: (args) => this.inspectTagStoreQuery(`node:sqlite.SQLTagStore.${func}`, args),
});
}
return returnValue;
},
});
}
});
}
inspectQuery(operation, args) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
if (args.length === 0 ||
typeof args[0] !== "string" ||
args[0].length === 0) {
return undefined;
}
const sql = args[0];
const sqlResult = (0, checkContextForSqlInjection_1.checkContextForSqlInjection)({
operation: operation,
sql: sql,
context: context,
dialect: this.dialect,
});
if (sqlResult) {
return sqlResult;
}
return (0, checkContextForIdor_1.checkContextForIdor)({
sql,
context,
dialect: this.dialect,
resolvePlaceholder: () =>
// node:sqlite does not support placeholders in exec
undefined,
});
}
addRawQueryToStatement(statement, args) {
if (args.length === 0 || typeof args[0] !== "string") {
return statement;
}
// Store the raw SQL query on the statement so we can use it later in the inspection.
// We can not use the existing sourceSQL or expandedSQL properties
// because e.g. comments get stripped out in those, and comments can be used in SQL injection attacks.
Object.defineProperty(statement, zenRawQuerySymbol, {
value: args[0],
enumerable: false,
configurable: false,
writable: false,
});
return statement;
}
inspectStatementQuery(operation, args, statement) {
const context = (0, Context_1.getContext)();
const rawQuery = statement[zenRawQuerySymbol];
if (!context || !rawQuery) {
return undefined;
}
const sqlResult = (0, checkContextForSqlInjection_1.checkContextForSqlInjection)({
operation: operation,
sql: rawQuery,
context: context,
dialect: this.dialect,
});
if (sqlResult) {
return sqlResult;
}
const { namedParameters, anonymousParameters } = this.extractStatementParameters(args);
return (0, checkContextForIdor_1.checkContextForIdor)({
sql: rawQuery,
context,
dialect: this.dialect,
resolvePlaceholder: (placeholder, placeholderNumber) => this.resolvePlaceholder(placeholder, placeholderNumber, namedParameters, anonymousParameters),
});
}
extractStatementParameters(args) {
if (args.length === 0) {
return {
namedParameters: undefined,
anonymousParameters: [],
};
}
if ((0, isPlainObject_1.isPlainObject)(args[0])) {
return {
namedParameters: args[0],
anonymousParameters: args.slice(1),
};
}
// When the first argument is not an object, node:sqlite binds all arguments as anonymous parameters.
// See anon_start in https://github.com/nodejs/node/blob/main/src/node_sqlite.cc
return {
namedParameters: undefined,
anonymousParameters: args,
};
}
resolvePlaceholder(placeholder, placeholderNumber, namedParameters, anonymousParameters) {
if (placeholder === "?" && placeholderNumber !== undefined) {
if (placeholderNumber < anonymousParameters.length) {
return anonymousParameters[placeholderNumber];
}
return undefined;
}
if (!namedParameters || placeholder.length <= 1) {
return undefined;
}
const prefix = placeholder[0];
if (prefix !== ":" && prefix !== "@" && prefix !== "$") {
return undefined;
}
const key = placeholder.slice(1);
// node:sqlite supports all three prefixes interchangeably, so we check for the key with and without the prefix.
if (Object.hasOwn(namedParameters, placeholder)) {
return namedParameters[placeholder];
}
if (Object.hasOwn(namedParameters, key)) {
return namedParameters[key];
}
return undefined;
}
isTaggedTemplate(obj) {
return Array.isArray(obj) && "raw" in obj && typeof obj.raw === "object";
}
inspectTagStoreQuery(operation, args) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
const { sql, params } = this.extractSqlAndParamsFromTaggedTemplate(args) || {};
if (typeof sql !== "string") {
return undefined;
}
const sqlResult = (0, checkContextForSqlInjection_1.checkContextForSqlInjection)({
operation: operation,
sql: sql,
context: context,
dialect: this.dialect,
});
if (sqlResult) {
return sqlResult;
}
return (0, checkContextForIdor_1.checkContextForIdor)({
sql: sql,
context,
dialect: this.dialect,
resolvePlaceholder: (placeholder, placeholderNumber) => {
if (placeholder === "?" && placeholderNumber !== undefined && params) {
if (placeholderNumber < params.length) {
return params[placeholderNumber];
}
}
return undefined;
},
});
}
extractSqlAndParamsFromTaggedTemplate(args) {
if (args.length === 0 || !this.isTaggedTemplate(args[0])) {
return undefined;
}
const strings = args[0];
const values = args.slice(1);
let sql = "";
for (let i = 0; i < strings.length; i++) {
sql += strings[i];
if (i < values.length) {
sql += "?";
}
}
return { sql, params: values };
}
}
exports.NodeSQLite = NodeSQLite;