From eb7f0856d051425ec65eb677901e4253a58279e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 25 Mar 2026 12:44:18 +0100 Subject: [PATCH 1/4] Fix handling of Maps in NoSQL injection detection --- library/sinks/MongoDB.tests.ts | 111 ++++++++++++++++++ library/sinks/MongoDB.ts | 5 +- .../detectNoSQLInjection.test.ts | 29 +++++ .../nosql-injection/detectNoSQLInjection.ts | 29 +++++ 4 files changed, 171 insertions(+), 3 deletions(-) diff --git a/library/sinks/MongoDB.tests.ts b/library/sinks/MongoDB.tests.ts index 57f7bea03..e7fc8d764 100644 --- a/library/sinks/MongoDB.tests.ts +++ b/library/sinks/MongoDB.tests.ts @@ -282,6 +282,117 @@ export function createMongoDBTests( "Zen has blocked a NoSQL injection: MongoDB.Collection.find(...) originating from body.name" ); } + + await collection.insertMany([ + { title: "abc" }, + { title: "another title" }, + { title: "yet another title" }, + ]); + + { + // Confirms that filtering with Maps is possible + const mapFilter = new Map(); + mapFilter.set("title", "abc"); + const mapFilteredDocuments = await collection.find(mapFilter).toArray(); + t.same( + mapFilteredDocuments.map((document) => document.title), + ["abc"] + ); + + // Confirms that filtering with nested Maps is possible + const mapSubFilter = new Map(); + mapSubFilter.set("$ne", "abc"); + const mainMapFilter = new Map(); + mainMapFilter.set("title", mapSubFilter); + const mapSubFilteredDocuments = await collection + .find(mainMapFilter) + .toArray(); + t.same( + mapSubFilteredDocuments.map((document) => document.title), + ["another title", "yet another title"] + ); + + // Confirms that map inside plain object is also working + const plainObjectWithMapFilter = { + title: new Map([["$ne", "abc"]]), + }; + const plainObjectWithMapFilteredDocuments = await collection + .find(plainObjectWithMapFilter) + .toArray(); + t.same( + plainObjectWithMapFilteredDocuments.map((document) => document.title), + ["another title", "yet another title"] + ); + } + + const mapError = await t.rejects(async () => { + await runWithContext(unsafeContext, () => { + const filter = new Map(); + filter.set("title", { $ne: null }); + return collection.find(filter).toArray(); + }); + }); + t.ok(mapError instanceof Error); + if (mapError instanceof Error) { + t.same( + mapError.message, + "Zen has blocked a NoSQL injection: MongoDB.Collection.find(...) originating from body.myTitle" + ); + } + + const mapError2 = await t.rejects(async () => { + await runWithContext( + { + ...unsafeContext, + body: { + title: new Map([["$ne", null]]), + }, + }, + () => { + const filter = new Map(); + filter.set("title", { $ne: null }); + return collection.find(filter).toArray(); + } + ); + }); + t.ok(mapError2 instanceof Error); + if (mapError2 instanceof Error) { + t.same( + mapError2.message, + "Zen has blocked a NoSQL injection: MongoDB.Collection.find(...) originating from body.title" + ); + } + + const mapError3 = await t.rejects(async () => { + await runWithContext(unsafeContext, () => { + const filter = { + title: new Map([["$ne", null]]), + }; + return collection.find(filter).toArray(); + }); + }); + t.ok(mapError3 instanceof Error); + if (mapError3 instanceof Error) { + t.same( + mapError3.message, + "Zen has blocked a NoSQL injection: MongoDB.Collection.find(...) originating from body.myTitle" + ); + } + + const mapError4 = await t.rejects(async () => { + await runWithContext(unsafeContext, () => { + const filter = new Map(); + filter.set("title", new Map([["$ne", null]])); + return collection.find(filter).toArray(); + }); + }); + t.ok(mapError4 instanceof Error); + if (mapError4 instanceof Error) { + t.same( + mapError4.message, + "Zen has blocked a NoSQL injection: MongoDB.Collection.find(...) originating from body.myTitle" + ); + } } catch (error: any) { t.fail(error.message); } finally { diff --git a/library/sinks/MongoDB.ts b/library/sinks/MongoDB.ts index 5287da658..5fbb28eb7 100644 --- a/library/sinks/MongoDB.ts +++ b/library/sinks/MongoDB.ts @@ -3,7 +3,6 @@ import { Hooks } from "../agent/hooks/Hooks"; import { InterceptorResult } from "../agent/hooks/InterceptorResult"; import type { WrapPackageInfo } from "../agent/hooks/WrapPackageInfo"; import { detectNoSQLInjection } from "../vulnerabilities/nosql-injection/detectNoSQLInjection"; -import { isPlainObject } from "../helpers/isPlainObject"; import { Context, getContext } from "../agent/Context"; import { Wrapper } from "../agent/Wrapper"; import { wrapExport } from "../agent/hooks/wrapExport"; @@ -147,7 +146,7 @@ export class MongoDB implements Wrapper { return undefined; } - if (args.length > 0 && isPlainObject(args[0])) { + if (args.length > 0) { const filter = args[0]; return this.inspectFilter( @@ -172,7 +171,7 @@ export class MongoDB implements Wrapper { return undefined; } - if (args.length > 1 && isPlainObject(args[1])) { + if (args.length > 1) { const filter = args[1]; return this.inspectFilter( diff --git a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts index 9f5d4b403..c1bb1f10c 100644 --- a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts +++ b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts @@ -879,3 +879,32 @@ t.test("not a valid injection attempt", async (t) => { } ); }); + +t.test("it works with Maps", async (t) => { + t.same( + detectNoSQLInjection( + createContext({ + body: new Map([ + ["username", "admin"], + [ + "test", + new Map([ + ["$ne", ""], + ["hello", "world"], + ]), + ], + ]), + }), + { + username: "admin", + test: { $ne: "", hello: "world" }, + } + ), + { + injection: true, + source: "body", + pathsToPayload: [".test"], + payload: { $ne: "" }, + } + ); +}); diff --git a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts index a4cf451c2..f368d25b8 100644 --- a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts +++ b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts @@ -65,6 +65,14 @@ function matchFilterPartInUser( return matchFilterPartInUser(userInput.join(), filterPart, pathToPayload); } + if (userInput instanceof Map) { + return matchFilterPartInUser( + mapToPlainObject(userInput), + filterPart, + pathToPayload + ); + } + return { match: false, }; @@ -127,6 +135,13 @@ function findFilterPartWithOperators( } } + if (partOfFilter instanceof Map) { + return findFilterPartWithOperators( + userInput, + mapToPlainObject(partOfFilter) + ); + } + return { found: false }; } @@ -143,6 +158,10 @@ export function detectNoSQLInjection( request: Context, filter: unknown ): DetectionResult { + if (filter instanceof Map) { + return detectNoSQLInjection(request, mapToPlainObject(filter)); + } + if (!isPlainObject(filter) && !Array.isArray(filter)) { return { injection: false }; } @@ -164,3 +183,13 @@ export function detectNoSQLInjection( return { injection: false }; } + +function mapToPlainObject(map: Map): Record { + const obj: Record = {}; + for (const [key, value] of map.entries()) { + if (typeof key === "string") { + obj[key] = value; + } + } + return obj; +} From f9293dc936a454e8ce45a2f4821ef0406bd6c8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 25 Mar 2026 12:45:02 +0100 Subject: [PATCH 2/4] Extract strings from more object types --- .../extractStringsFromUserInput.test.ts | 97 +++++++++++++++++++ .../helpers/extractStringsFromUserInput.ts | 42 ++++++++ 2 files changed, 139 insertions(+) diff --git a/library/helpers/extractStringsFromUserInput.test.ts b/library/helpers/extractStringsFromUserInput.test.ts index 8804a704d..b580779a8 100644 --- a/library/helpers/extractStringsFromUserInput.test.ts +++ b/library/helpers/extractStringsFromUserInput.test.ts @@ -345,3 +345,100 @@ t.test("it works with objects containing constructor key", async () => { fromArr(["test", "value", "constructor", "constructor value"]) ); }); + +t.test("it works with objects containing prototype key", async () => { + t.same( + extractStringsFromUserInput({ + test: "value", + prototype: "prototype value", + }), + fromArr(["test", "value", "prototype", "prototype value"]) + ); + + t.same( + extractStringsFromUserInput({ + test: "value", + __proto__: { protoKey: "protoValue" }, + }), + fromArr(["test", "value", "protoKey", "protoValue"]) + ); +}); + +t.test("it works with Map objects", async () => { + const map = new Map(); + map.set("key1", "value1"); + map.set("key2", { nestedKey: "nestedValue" }); + map.set(5, "value3"); + + t.same( + extractStringsFromUserInput(map), + fromArr(["key1", "value1", "key2", "nestedKey", "nestedValue", "value3"]) + ); +}); + +t.test("it works with Sets", async () => { + const set = new Set(); + set.add("value1"); + set.add({ nestedKey: "nestedValue" }); + + t.same( + extractStringsFromUserInput(set), + fromArr(["value1", "nestedKey", "nestedValue"]) + ); +}); + +t.test("it works with URLSearchParams", async () => { + const params = new URLSearchParams(); + params.append("key1", "value1"); + params.append("key2", "value2"); + + t.same( + extractStringsFromUserInput(params), + fromArr(["key1", "value1", "key2", "value2"]) + ); +}); + +t.test( + "it works with FormData", + { + skip: + typeof globalThis.FormData === "undefined" + ? "FormData is not supported in this environment" + : false, + }, + async () => { + const formData = new globalThis.FormData(); + formData.append("key1", "value1"); + formData.append("key2", "value2"); + + t.same( + extractStringsFromUserInput(formData), + fromArr(["key1", "value1", "key2", "value2"]) + ); + } +); + +t.test( + "it works with headers object", + { + skip: + typeof Headers === "undefined" + ? "Headers is not supported in this environment" + : false, + }, + async () => { + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + headers.append("Authorization", "Bearer token"); + + t.same( + extractStringsFromUserInput(headers), + fromArr([ + "content-type", + "application/json", + "authorization", + "Bearer token", + ]) + ); + } +); diff --git a/library/helpers/extractStringsFromUserInput.ts b/library/helpers/extractStringsFromUserInput.ts index d8f1dd761..13dba2e8f 100644 --- a/library/helpers/extractStringsFromUserInput.ts +++ b/library/helpers/extractStringsFromUserInput.ts @@ -30,6 +30,48 @@ export function extractStringsFromUserInput( } } + if (obj instanceof Map) { + for (const [key, value] of obj.entries()) { + if (typeof key === "string") { + results.add(key); + } + extractStringsFromUserInput(value, depth + 1).forEach((v) => + results.add(v) + ); + } + } + + if (obj instanceof Set) { + for (const value of obj.values()) { + extractStringsFromUserInput(value, depth + 1).forEach((v) => + results.add(v) + ); + } + } + + if (obj instanceof URLSearchParams) { + for (const [key, value] of obj.entries()) { + results.add(key); + results.add(value); + } + } + + if (globalThis.FormData && obj instanceof globalThis.FormData) { + obj.forEach((value, key) => { + results.add(key); + if (typeof value === "string") { + results.add(value); + } + }); + } + + if (globalThis.Headers && obj instanceof globalThis.Headers) { + obj.forEach((value, key) => { + results.add(key); + results.add(value); + }); + } + if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { extractStringsFromUserInput(obj[i], depth + 1).forEach((value) => From 6d907cdc3e2b6f27f4885ddc3005e37e3efc6c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 27 Mar 2026 17:31:33 +0100 Subject: [PATCH 3/4] Also extract strings from non plain objects --- .../extractStringsFromUserInput.test.ts | 71 ++++++++++++++++--- .../helpers/extractStringsFromUserInput.ts | 45 +++++++++--- 2 files changed, 98 insertions(+), 18 deletions(-) diff --git a/library/helpers/extractStringsFromUserInput.test.ts b/library/helpers/extractStringsFromUserInput.test.ts index b580779a8..1ee71e685 100644 --- a/library/helpers/extractStringsFromUserInput.test.ts +++ b/library/helpers/extractStringsFromUserInput.test.ts @@ -100,12 +100,10 @@ t.test("it decodes JWTs", async () => { }), fromArr([ "token", - "iat", - "username", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOnsiJG5lIjpudWxsfSwiaWF0IjoxNTE2MjM5MDIyfQ._jhGJw9WzB6gHKPSozTFHDo9NOHs3CNOlvJ8rWy6VrQ", "sub", "1234567890", "$ne", - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOnsiJG5lIjpudWxsfSwiaWF0IjoxNTE2MjM5MDIyfQ._jhGJw9WzB6gHKPSozTFHDo9NOHs3CNOlvJ8rWy6VrQ", "username", "iat", ]) @@ -356,11 +354,10 @@ t.test("it works with objects containing prototype key", async () => { ); t.same( - extractStringsFromUserInput({ - test: "value", - __proto__: { protoKey: "protoValue" }, - }), - fromArr(["test", "value", "protoKey", "protoValue"]) + extractStringsFromUserInput( + JSON.parse('{"test":"value","__proto__":{"protoKey":"protoValue"}}') + ), + fromArr(["test", "value", "__proto__", "protoKey", "protoValue"]) ); }); @@ -442,3 +439,61 @@ t.test( ); } ); + +t.test("it works with class instances", async () => { + class Foo { + bar = "baz"; + num = 42; + } + t.same( + extractStringsFromUserInput(new Foo()), + fromArr(["bar", "baz", "num"]) + ); + + class Nested { + key = "value"; + child = { nested: "data" }; + } + t.same( + extractStringsFromUserInput(new Nested()), + fromArr(["key", "value", "child", "nested", "data"]) + ); +}); + +t.test("it works with wrongly used WeakMap", async () => { + const weakMap = new WeakMap(); + // @ts-expect-error Ignore + weakMap["key1"] = "value1"; + // @ts-expect-error Ignore + weakMap["key2"] = { nestedKey: "nestedValue" }; + + t.same( + extractStringsFromUserInput(weakMap), + fromArr(["key1", "value1", "key2", "nestedKey", "nestedValue"]) + ); +}); + +t.test("it does not call getters", async () => { + const obj = { + get secret() { + return "should not be called"; + }, + normalKey: "normalValue", + }; + + t.same( + extractStringsFromUserInput(obj), + fromArr(["normalKey", "normalValue"]) + ); +}); + +t.test("it does not call toString methods", async () => { + const obj = { + key: "value", + toString() { + throw new Error("toString should not be called"); + }, + }; + + t.same(extractStringsFromUserInput(obj), fromArr(["key", "value"])); +}); diff --git a/library/helpers/extractStringsFromUserInput.ts b/library/helpers/extractStringsFromUserInput.ts index 13dba2e8f..5d13be1c3 100644 --- a/library/helpers/extractStringsFromUserInput.ts +++ b/library/helpers/extractStringsFromUserInput.ts @@ -1,4 +1,3 @@ -import { isPlainObject } from "./isPlainObject"; import { safeDecodeURIComponent } from "./safeDecodeURIComponent"; import { tryDecodeAsJWT } from "./tryDecodeAsJWT"; import { tryParseURL } from "./tryParseURL"; @@ -21,15 +20,6 @@ export function extractStringsFromUserInput( return results; } - if (isPlainObject(obj)) { - for (const key in obj) { - results.add(key); - extractStringsFromUserInput(obj[key], depth + 1).forEach((value) => { - results.add(value); - }); - } - } - if (obj instanceof Map) { for (const [key, value] of obj.entries()) { if (typeof key === "string") { @@ -39,6 +29,7 @@ export function extractStringsFromUserInput( results.add(v) ); } + return results; } if (obj instanceof Set) { @@ -47,6 +38,7 @@ export function extractStringsFromUserInput( results.add(v) ); } + return results; } if (obj instanceof URLSearchParams) { @@ -54,6 +46,7 @@ export function extractStringsFromUserInput( results.add(key); results.add(value); } + return results; } if (globalThis.FormData && obj instanceof globalThis.FormData) { @@ -63,6 +56,7 @@ export function extractStringsFromUserInput( results.add(value); } }); + return results; } if (globalThis.Headers && obj instanceof globalThis.Headers) { @@ -70,6 +64,7 @@ export function extractStringsFromUserInput( results.add(key); results.add(value); }); + return results; } if (Array.isArray(obj)) { @@ -88,6 +83,7 @@ export function extractStringsFromUserInput( // Ignore deeply nested/cyclic arrays that can overflow during native join recursion. // We still keep strings gathered from traversed elements above. } + return results; } if (typeof obj === "string" && obj.length > 0) { @@ -117,6 +113,35 @@ export function extractStringsFromUserInput( results.add(value); }); } + return results; + } + + if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) { + try { + for (const key of Reflect.ownKeys(obj)) { + const descriptor = Object.getOwnPropertyDescriptor(obj, key); + if (!descriptor || !("value" in descriptor)) { + continue; + } + + // Ignore function names + if (typeof descriptor.value === "function") { + continue; + } + + if (typeof key === "string" && key.length > 0) { + results.add(key); + } + + extractStringsFromUserInput(descriptor.value, depth + 1).forEach( + (value) => { + results.add(value); + } + ); + } + } catch { + // Ignore errors + } } return results; From d1e099f0a3f128f33934b1b937ad1dc85d44ef77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 27 Mar 2026 17:46:14 +0100 Subject: [PATCH 4/4] Prevent NoSQL injections with complex objects --- library/sinks/MongoDB.tests.ts | 16 +++ .../detectNoSQLInjection.test.ts | 108 ++++++++++++++++++ .../nosql-injection/detectNoSQLInjection.ts | 60 ++++++++-- 3 files changed, 173 insertions(+), 11 deletions(-) diff --git a/library/sinks/MongoDB.tests.ts b/library/sinks/MongoDB.tests.ts index e7fc8d764..214f67776 100644 --- a/library/sinks/MongoDB.tests.ts +++ b/library/sinks/MongoDB.tests.ts @@ -393,6 +393,22 @@ export function createMongoDBTests( "Zen has blocked a NoSQL injection: MongoDB.Collection.find(...) originating from body.myTitle" ); } + + const weakMapError = await t.rejects(async () => { + await runWithContext(unsafeContext, () => { + const filter = new WeakMap(); + // @ts-expect-error Wrongly used WeakMap + filter["title"] = { $ne: null }; + return collection.find(filter).toArray(); + }); + }); + t.ok(weakMapError instanceof Error); + if (weakMapError instanceof Error) { + t.same( + weakMapError.message, + "Zen has blocked a NoSQL injection: MongoDB.Collection.find(...) originating from body.myTitle" + ); + } } catch (error: any) { t.fail(error.message); } finally { diff --git a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts index c1bb1f10c..25e03476f 100644 --- a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts +++ b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.test.ts @@ -908,3 +908,111 @@ t.test("it works with Maps", async (t) => { } ); }); + +t.test("it works with non plain object filter", async (t) => { + class Filter { + username = "admin"; + test = { $ne: "", hello: "world" }; + } + + t.same( + detectNoSQLInjection( + createContext({ + body: { + username: "admin", + test: { $ne: "", hello: "world" }, + }, + }), + new Filter() + ), + { + injection: true, + source: "body", + pathsToPayload: [".test"], + payload: { $ne: "" }, + } + ); + + class Operators { + $gt = "21"; + $lt = "100"; + } + + t.same( + detectNoSQLInjection( + createContext({ + body: { age: { $gt: "21", $lt: "100" } }, + }), + { + age: new Operators(), + } + ), + { + injection: true, + source: "body", + pathsToPayload: [".age"], + payload: { $gt: "21", $lt: "100" }, + } + ); +}); + +t.test( + "it ignores inherited properties on non plain object filter", + async (t) => { + const inherited = { + test: { $ne: "" }, + }; + + const filter = Object.create(inherited) as { + username: string; + }; + filter.username = "admin"; + + t.same( + detectNoSQLInjection( + createContext({ + body: { + username: "admin", + test: { $ne: "" }, + }, + }), + filter + ), + { + injection: false, + } + ); + } +); + +t.test("it ignores getter properties on non plain object filter", async (t) => { + const filter: Record = { + username: "admin", + test: { $ne: "" }, + }; + + Object.defineProperty(filter, "throwing", { + enumerable: true, + get() { + throw new Error("getter should not be executed"); + }, + }); + + t.same( + detectNoSQLInjection( + createContext({ + body: { + username: "admin", + test: { $ne: "" }, + }, + }), + filter + ), + { + injection: true, + source: "body", + pathsToPayload: [".test"], + payload: { $ne: "" }, + } + ); +}); diff --git a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts index f368d25b8..ebbb7a46c 100644 --- a/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts +++ b/library/vulnerabilities/nosql-injection/detectNoSQLInjection.ts @@ -2,10 +2,47 @@ import { isDeepStrictEqual } from "util"; import { Context } from "../../agent/Context"; import { Source, SOURCES } from "../../agent/Source"; import { buildPathToPayload, PathPart } from "../../helpers/attackPath"; -import { isPlainObject } from "../../helpers/isPlainObject"; import { tryDecodeAsJWT } from "../../helpers/tryDecodeAsJWT"; import { detectDbJsInjection } from "../js-injection/detectDbJsInjection"; +function isObjectLike(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + !(value instanceof Map) + ); +} + +function getOwnStringDataProperties( + obj: Record +): Array<{ key: string; value: unknown }> { + const properties: Array<{ key: string; value: unknown }> = []; + + try { + for (const key of Reflect.ownKeys(obj)) { + if (typeof key !== "string") { + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor(obj, key); + if (!descriptor || !("value" in descriptor)) { + continue; + } + + if (typeof descriptor.value === "function") { + continue; + } + + properties.push({ key, value: descriptor.value }); + } + } catch { + return properties; + } + + return properties; +} + function matchFilterPartInUser( userInput: unknown, filterPart: Record, @@ -30,17 +67,17 @@ function matchFilterPartInUser( } } - if (isPlainObject(userInput)) { + if (isObjectLike(userInput)) { const filteredInput = removeKeysThatDontStartWithDollarSign(userInput); if (isDeepStrictEqual(filteredInput, filterPart)) { return { match: true, pathToPayload: buildPathToPayload(pathToPayload) }; } - for (const key in userInput) { + for (const property of getOwnStringDataProperties(userInput)) { const match = matchFilterPartInUser( - userInput[key], + property.value, filterPart, - pathToPayload.concat([{ type: "object", key: key }]) + pathToPayload.concat([{ type: "object", key: property.key }]) ); if (match.match) { @@ -81,9 +118,10 @@ function matchFilterPartInUser( function removeKeysThatDontStartWithDollarSign( filter: Record ): Record { - return Object.keys(filter).reduce((acc, key) => { + return getOwnStringDataProperties(filter).reduce((acc, property) => { + const key = property.key; if (key.startsWith("$")) { - return { ...acc, [key]: filter[key] }; + return { ...acc, [key]: property.value }; } return acc; @@ -94,7 +132,7 @@ function findFilterPartWithOperators( userInput: unknown, partOfFilter: unknown ): { found: false } | { found: true; pathToPayload: string; payload: unknown } { - if (isPlainObject(partOfFilter)) { + if (isObjectLike(partOfFilter)) { const object = removeKeysThatDontStartWithDollarSign(partOfFilter); if (Object.keys(object).length > 0) { const result = matchFilterPartInUser(userInput, object); @@ -108,8 +146,8 @@ function findFilterPartWithOperators( } } - for (const key in partOfFilter) { - const result = findFilterPartWithOperators(userInput, partOfFilter[key]); + for (const property of getOwnStringDataProperties(partOfFilter)) { + const result = findFilterPartWithOperators(userInput, property.value); if (result.found) { return { @@ -162,7 +200,7 @@ export function detectNoSQLInjection( return detectNoSQLInjection(request, mapToPlainObject(filter)); } - if (!isPlainObject(filter) && !Array.isArray(filter)) { + if (!isObjectLike(filter) && !Array.isArray(filter)) { return { injection: false }; }