@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.
140 lines (139 loc) • 5.46 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectNoSQLInjection = detectNoSQLInjection;
const util_1 = require("util");
const Source_1 = require("../../agent/Source");
const attackPath_1 = require("../../helpers/attackPath");
const isPlainObject_1 = require("../../helpers/isPlainObject");
const tryDecodeAsJWT_1 = require("../../helpers/tryDecodeAsJWT");
const detectDbJsInjection_1 = require("../js-injection/detectDbJsInjection");
// Matches the depth limit used by extractStringsFromUserInput
const MAX_DEPTH = 1024;
function matchFilterPartInUser(userInput, filterPart, pathToPayload = [], depth = 0) {
if (depth > MAX_DEPTH) {
return { match: false };
}
if (typeof userInput === "string") {
// Check for js injection in $where
if ((0, detectDbJsInjection_1.detectDbJsInjection)(userInput, filterPart)) {
return {
match: true,
pathToPayload: (0, attackPath_1.buildPathToPayload)(pathToPayload),
};
}
const jwt = (0, tryDecodeAsJWT_1.tryDecodeAsJWT)(userInput);
if (jwt.jwt) {
return matchFilterPartInUser(jwt.object, filterPart, pathToPayload.concat([{ type: "jwt" }]), depth + 1);
}
}
if ((0, isPlainObject_1.isPlainObject)(userInput)) {
const filteredInput = removeKeysThatDontStartWithDollarSign(userInput);
if (isUserOperatorsSubsetOf(filteredInput, filterPart)) {
return { match: true, pathToPayload: (0, attackPath_1.buildPathToPayload)(pathToPayload) };
}
for (const key in userInput) {
const match = matchFilterPartInUser(userInput[key], filterPart, pathToPayload.concat([{ type: "object", key: key }]), depth + 1);
if (match.match) {
return match;
}
}
}
if (Array.isArray(userInput)) {
for (let index = 0; index < userInput.length; index++) {
const match = matchFilterPartInUser(userInput[index], filterPart, pathToPayload.concat([{ type: "array", index: index }]), depth + 1);
if (match.match) {
return match;
}
}
try {
return matchFilterPartInUser(userInput.join(), filterPart, pathToPayload, depth + 1);
}
catch {
// Ignore deeply nested arrays that overflow during native join recursion.
}
}
return {
match: false,
};
}
function removeKeysThatDontStartWithDollarSign(filter) {
return Object.keys(filter).reduce((acc, key) => {
if (key.startsWith("$")) {
return { ...acc, [key]: filter[key] };
}
return acc;
}, {});
}
// Returns true if every operator in userOperators is present in filterOperators
// with the same value — i.e. the user-supplied operators are a subset of the
// filter. An empty userOperators object never matches (no operators = no injection).
function isUserOperatorsSubsetOf(userOperators, filterOperators) {
let hasKeys = false;
for (const key in userOperators) {
// Any missing key or value mismatch means the user input wasn't used as-is.
if (!(key in filterOperators) ||
!(0, util_1.isDeepStrictEqual)(userOperators[key], filterOperators[key])) {
return false;
}
// Only count the key as seen after it passes the check above.
hasKeys = true;
}
return hasKeys;
}
function findFilterPartWithOperators(userInput, partOfFilter) {
if ((0, isPlainObject_1.isPlainObject)(partOfFilter)) {
const object = removeKeysThatDontStartWithDollarSign(partOfFilter);
if (Object.keys(object).length > 0) {
const result = matchFilterPartInUser(userInput, object);
if (result.match) {
return {
found: true,
pathToPayload: result.pathToPayload,
payload: object,
};
}
}
for (const key in partOfFilter) {
const result = findFilterPartWithOperators(userInput, partOfFilter[key]);
if (result.found) {
return {
found: true,
pathToPayload: result.pathToPayload,
payload: result.payload,
};
}
}
}
if (Array.isArray(partOfFilter)) {
for (const value of partOfFilter) {
const result = findFilterPartWithOperators(userInput, value);
if (result.found) {
return {
found: true,
pathToPayload: result.pathToPayload,
payload: result.payload,
};
}
}
}
return { found: false };
}
function detectNoSQLInjection(request, filter) {
if (!(0, isPlainObject_1.isPlainObject)(filter) && !Array.isArray(filter)) {
return { injection: false };
}
for (const source of Source_1.SOURCES) {
if (request[source]) {
const result = findFilterPartWithOperators(request[source], filter);
if (result.found) {
return {
injection: true,
source: source,
pathsToPayload: [result.pathToPayload],
payload: result.payload,
};
}
}
}
return { injection: false };
}