@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.
247 lines (246 loc) • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoDB = void 0;
const detectNoSQLInjection_1 = require("../vulnerabilities/nosql-injection/detectNoSQLInjection");
const isPlainObject_1 = require("../helpers/isPlainObject");
const Context_1 = require("../agent/Context");
const wrapExport_1 = require("../agent/hooks/wrapExport");
const OPERATIONS_WITH_FILTER = [
"count",
"countDocuments",
"find",
"findOne",
"findOneAndUpdate",
"findOneAndReplace",
"findOneAndDelete",
"deleteOne",
"deleteMany",
"updateOne",
"updateMany",
"replaceOne",
];
// Write operations that also carry user-controlled content in args[1]
const OPERATIONS_WITH_UPDATE_ARG = new Set([
"findOneAndUpdate",
"findOneAndReplace",
"updateOne",
"updateMany",
"replaceOne",
]);
const BULK_WRITE_OPERATIONS_WITH_FILTER = [
"replaceOne",
"updateOne",
"updateMany",
"deleteOne",
"deleteMany",
];
class MongoDB {
inspectFilter(db, collection, request, filter, operation) {
const result = (0, detectNoSQLInjection_1.detectNoSQLInjection)(request, filter);
if (result.injection) {
return {
operation: `MongoDB.Collection.${operation}`,
kind: "nosql_injection",
source: result.source,
pathsToPayload: result.pathsToPayload,
metadata: {
db: db,
collection: collection,
operation: operation,
filter: JSON.stringify(filter),
},
payload: result.payload,
};
}
}
inspectBulkWriteOperation(operation, collection, context) {
for (const op of BULK_WRITE_OPERATIONS_WITH_FILTER) {
const options = operation[op];
if (!options) {
continue;
}
if (options.filter) {
const result = this.inspectFilter(collection.dbName, collection.collectionName, context, options.filter, "bulkWrite");
if (result) {
return result;
}
}
// Also scan update (updateOne/updateMany) and replacement (replaceOne).
for (const updateContent of [options.update, options.replacement]) {
if ((0, isPlainObject_1.isPlainObject)(updateContent) || Array.isArray(updateContent)) {
const result = this.inspectFilter(collection.dbName, collection.collectionName, context, updateContent, "bulkWrite");
if (result) {
return result;
}
}
}
}
return undefined;
}
inspectBulkOpFind(args, bulkOp) {
var _a, _b;
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
// v6+: this.collection; v4/v5: this.s.collection
const bulkOpAny = bulkOp;
const collection = ((_a = bulkOpAny === null || bulkOpAny === void 0 ? void 0 : bulkOpAny.collection) !== null && _a !== void 0 ? _a : (_b = bulkOpAny === null || bulkOpAny === void 0 ? void 0 : bulkOpAny.s) === null || _b === void 0 ? void 0 : _b.collection);
if (!collection) {
return undefined;
}
if (args.length > 0 && (0, isPlainObject_1.isPlainObject)(args[0])) {
return this.inspectFilter(collection.dbName, collection.collectionName, context, args[0], "find");
}
return undefined;
}
inspectBulkWrite(args, collection) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
if (Array.isArray(args[0]) && args[0].length > 0) {
const operations = args[0];
for (const operation of operations) {
const result = this.inspectBulkWriteOperation(operation, collection, context);
if (result) {
return result;
}
}
}
return undefined;
}
inspectAggregate(args, collection) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
if (Array.isArray(args) && args.length > 0) {
const pipeline = args[0];
return this.inspectFilter(collection.dbName, collection.collectionName, context, pipeline, "aggregate");
}
return undefined;
}
inspectOperation(operation, args, collection) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
if (args.length > 0 && (0, isPlainObject_1.isPlainObject)(args[0])) {
const result = this.inspectFilter(collection.dbName, collection.collectionName, context, args[0], operation);
if (result) {
return result;
}
}
if (OPERATIONS_WITH_UPDATE_ARG.has(operation) &&
args.length > 1 &&
((0, isPlainObject_1.isPlainObject)(args[1]) || Array.isArray(args[1]))) {
return this.inspectFilter(collection.dbName, collection.collectionName, context, args[1], operation);
}
return undefined;
}
inspectDistinct(args, collection) {
const context = (0, Context_1.getContext)();
if (!context) {
return undefined;
}
if (args.length > 1 && (0, isPlainObject_1.isPlainObject)(args[1])) {
const filter = args[1];
return this.inspectFilter(collection.dbName, collection.collectionName, context, filter, "distinct");
}
return undefined;
}
wrapCollection(exports, pkgInfo) {
var _a;
const collectionProto = exports.Collection.prototype;
OPERATIONS_WITH_FILTER.forEach((operation) => {
(0, wrapExport_1.wrapExport)(collectionProto, operation, pkgInfo, {
kind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectOperation(operation, args, collection),
});
});
(0, wrapExport_1.wrapExport)(collectionProto, "bulkWrite", pkgInfo, {
kind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectBulkWrite(args, collection),
});
(0, wrapExport_1.wrapExport)(collectionProto, "aggregate", pkgInfo, {
kind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectAggregate(args, collection),
});
(0, wrapExport_1.wrapExport)(collectionProto, "distinct", pkgInfo, {
kind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectDistinct(args, collection),
});
// BulkOperationBase.prototype.find is not on Collection but on the bulk op
// classes returned by initializeOrderedBulkOp / initializeUnorderedBulkOp.
// Both subclasses inherit find from BulkOperationBase, so we patch it once
// on the shared base prototype.
if ((_a = exports.OrderedBulkOperation) === null || _a === void 0 ? void 0 : _a.prototype) {
const bulkOpBaseProto = Object.getPrototypeOf(exports.OrderedBulkOperation.prototype);
if (bulkOpBaseProto === null || bulkOpBaseProto === void 0 ? void 0 : bulkOpBaseProto.find) {
(0, wrapExport_1.wrapExport)(bulkOpBaseProto, "find", pkgInfo, {
kind: "nosql_op",
inspectArgs: (args, agent, bulkOp) => this.inspectBulkOpFind(args, bulkOp),
});
}
}
}
wrap(hooks) {
hooks
.addPackage("mongodb")
.withVersion("^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0")
.onRequire((exports, pkgInfo) => {
// From mongodb v6.10.0, the Collection is undefined
// It's defined like:
// exports.Collection = void 0;
// const collection_1 = require("./collection");
// Object.defineProperty(exports, "Collection", { enumerable: true, get: function () { return collection_1.Collection; } });
// So we need to wait for the next tick to wrap the Collection
process.nextTick(() => {
this.wrapCollection(exports, pkgInfo);
});
})
.addFileInstrumentation({
path: "lib/collection.js",
functions: [
...OPERATIONS_WITH_FILTER.map((operation) => ({
name: operation,
nodeType: "MethodDefinition",
operationKind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectOperation(operation, args, collection),
})),
{
name: "bulkWrite",
nodeType: "MethodDefinition",
operationKind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectBulkWrite(args, collection),
},
{
name: "aggregate",
nodeType: "MethodDefinition",
operationKind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectAggregate(args, collection),
},
{
name: "distinct",
nodeType: "MethodDefinition",
operationKind: "nosql_op",
inspectArgs: (args, agent, collection) => this.inspectDistinct(args, collection),
},
],
})
.addFileInstrumentation({
path: "lib/bulk/common.js",
functions: [
{
name: "find",
nodeType: "MethodDefinition",
className: "BulkOperationBase",
operationKind: "nosql_op",
inspectArgs: (args, agent, bulkOp) => this.inspectBulkOpFind(args, bulkOp),
},
],
});
}
}
exports.MongoDB = MongoDB;