Skip to content

Commit

Permalink
Feedback detectNoSQLInjection
Browse files Browse the repository at this point in the history
  • Loading branch information
hansott committed Feb 14, 2024
1 parent cc12f19 commit 9a7b1fa
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 74 deletions.
4 changes: 2 additions & 2 deletions library/src/integrations/MongoDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class MongoDB implements Integration {
source: result.source,
request: request,
stack: new Error().stack || "",
path: result.path,
path: result.pathToPayload,
metadata: {
db: db,
collection: collection,
Expand All @@ -79,7 +79,7 @@ export class MongoDB implements Integration {

if (agent.shouldBlock()) {
throw new Error(
`Aikido guard has blocked a NoSQL injection: MongoDB.Collection.${operation}(...) originating from ${friendlyName(result.source)} (${result.path})`
`Aikido guard has blocked a NoSQL injection: MongoDB.Collection.${operation}(...) originating from ${friendlyName(result.source)} (${result.pathToPayload})`
);
}
}
Expand Down
38 changes: 19 additions & 19 deletions library/src/vulnerabilities/detectNoSQLInjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ t.test("using $ne in query parameter", async (t) => {
title: { $ne: null },
}
),
{ injection: true, source: "query", path: ".title" }
{ injection: true, source: "query", pathToPayload: ".title" }
);
});

Expand Down Expand Up @@ -122,7 +122,7 @@ t.test("using $ne in body", async (t) => {
title: { $ne: null },
}
),
{ injection: true, source: "body", path: ".title" }
{ injection: true, source: "body", pathToPayload: ".title" }
);
});

Expand All @@ -136,7 +136,7 @@ t.test("using $ne in body (different name)", async (t) => {
myTitle: { $ne: null },
}
),
{ injection: true, source: "body", path: ".title" }
{ injection: true, source: "body", pathToPayload: ".title" }
);
});

Expand All @@ -150,7 +150,7 @@ t.test("using $ne in headers with different name", async (t) => {
someField: { $ne: null },
}
),
{ injection: true, source: "body", path: ".title" }
{ injection: true, source: "body", pathToPayload: ".title" }
);
});

Expand All @@ -171,7 +171,7 @@ t.test("using $ne inside $and", async (t) => {
],
}
),
{ injection: true, source: "body", path: ".title" }
{ injection: true, source: "body", pathToPayload: ".title" }
);
});

Expand All @@ -192,7 +192,7 @@ t.test("using $ne inside $or", async (t) => {
],
}
),
{ injection: true, source: "body", path: ".title" }
{ injection: true, source: "body", pathToPayload: ".title" }
);
});

Expand All @@ -213,7 +213,7 @@ t.test("using $ne inside $nor", async (t) => {
],
}
),
{ injection: true, source: "body", path: ".title" }
{ injection: true, source: "body", pathToPayload: ".title" }
);
});

Expand All @@ -229,7 +229,7 @@ t.test("using $ne inside $not", async (t) => {
},
}
),
{ injection: true, source: "body", path: ".title" }
{ injection: true, source: "body", pathToPayload: ".title" }
);
});

Expand All @@ -248,7 +248,7 @@ t.test("using $ne nested in body", async (t) => {
{
injection: true,
source: "body",
path: ".nested.nested",
pathToPayload: ".nested.nested",
}
);
});
Expand Down Expand Up @@ -279,7 +279,7 @@ t.test("using $ne in JWT in headers", async (t) => {
{
injection: true,
source: "headers",
path: ".Authorization<jwt>.username",
pathToPayload: ".Authorization<jwt>.username",
}
);
});
Expand Down Expand Up @@ -310,7 +310,7 @@ t.test("using $ne in JWT in bearer header", async (t) => {
{
injection: true,
source: "headers",
path: ".Authorization<jwt>.username",
pathToPayload: ".Authorization<jwt>.username",
}
);
});
Expand Down Expand Up @@ -341,7 +341,7 @@ t.test("using $ne in JWT in cookies", async (t) => {
{
injection: true,
source: "cookies",
path: ".session<jwt>.username",
pathToPayload: ".session<jwt>.username",
}
);
});
Expand Down Expand Up @@ -375,7 +375,7 @@ t.test("using $gt in query parameter", async (t) => {
age: { $gt: "21" },
}
),
{ injection: true, source: "query", path: ".age" }
{ injection: true, source: "query", pathToPayload: ".age" }
);
});

Expand All @@ -389,7 +389,7 @@ t.test("using $gt and $lt in query parameter", async (t) => {
age: { $gt: "21", $lt: "100" },
}
),
{ injection: true, source: "body", path: ".age" }
{ injection: true, source: "body", pathToPayload: ".age" }
);
});

Expand All @@ -403,7 +403,7 @@ t.test("using $gt and $lt in query parameter (different name)", async (t) => {
myAge: { $gt: "21", $lt: "100" },
}
),
{ injection: true, source: "body", path: ".age" }
{ injection: true, source: "body", pathToPayload: ".age" }
);
});

Expand All @@ -425,7 +425,7 @@ t.test("using $gt and $lt in query parameter (nested)", async (t) => {
],
}
),
{ injection: true, source: "body", path: ".nested.nested.age" }
{ injection: true, source: "body", pathToPayload: ".nested.nested.age" }
);
});

Expand All @@ -449,7 +449,7 @@ t.test("using $gt and $lt in query parameter (root)", async (t) => {
],
}
),
{ injection: true, source: "body", path: "." }
{ injection: true, source: "body", pathToPayload: "." }
);
});

Expand All @@ -473,7 +473,7 @@ t.test("$where", async (t) => {
],
}
),
{ injection: true, source: "body", path: "." }
{ injection: true, source: "body", pathToPayload: "." }
);
});

Expand All @@ -495,7 +495,7 @@ t.test("array body", async (t) => {
],
}
),
{ injection: true, source: "body", path: ".[0]" }
{ injection: true, source: "body", pathToPayload: ".[0]" }
);
});

Expand Down
129 changes: 76 additions & 53 deletions library/src/vulnerabilities/detectNoSQLInjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,87 @@ import { tryDecodeAsJWT } from "../helpers/jwt";
import { Context } from "../agent/Context";
import { Source } from "../agent/Source";

type DetectionResult =
| { injection: true; source: Source; path: string }
| { injection: false };
type PathPart =
| { type: "jwt" }
| { type: "object"; key: string }
| { type: "array"; index: number };

function buildPathToPayload(pathToPayload: PathPart[]): string {
if (pathToPayload.length === 0) {
return ".";
}

return pathToPayload.reduce((acc, part) => {
if (part.type === "jwt") {
return `${acc}<jwt>`;
}

if (part.type === "object") {
return `${acc}.${part.key}`;
}

if (part.type === "array") {
return `${acc}.[${part.index}]`;
}

return acc;

Check warning on line 30 in library/src/vulnerabilities/detectNoSQLInjection.ts

View check run for this annotation

Codecov / codecov/patch

library/src/vulnerabilities/detectNoSQLInjection.ts#L30

Added line #L30 was not covered by tests
}, "");
}

function matchFilterPartInUser(
user: unknown,
userInput: unknown,
filterPart: Record<string, unknown>,
path = ""
): string | null {
if (typeof user === "string") {
const jwt = tryDecodeAsJWT(user);
pathToPayload: PathPart[] = []
): { match: false } | { match: true; pathToPayload: string } {
if (typeof userInput === "string") {
const jwt = tryDecodeAsJWT(userInput);
if (jwt.jwt) {
return matchFilterPartInUser(jwt.object, filterPart, `${path}<jwt>`);
return matchFilterPartInUser(
jwt.object,
filterPart,
pathToPayload.concat([{ type: "jwt" }])
);
}
}

if (isDeepStrictEqual(user, filterPart)) {
return path;
if (isDeepStrictEqual(userInput, filterPart)) {
return { match: true, pathToPayload: buildPathToPayload(pathToPayload) };
}

if (isPlainObject(user)) {
for (const key in user) {
if (isPlainObject(userInput)) {
for (const key in userInput) {
const match = matchFilterPartInUser(
user[key],
userInput[key],
filterPart,
`${path}.${key}`
pathToPayload.concat([{ type: "object", key: key }])
);

if (match) {
if (match.match) {
return match;
}
}
}

if (Array.isArray(user)) {
for (let index = 0; index < user.length; index++) {
if (Array.isArray(userInput)) {
for (let index = 0; index < userInput.length; index++) {
const match = matchFilterPartInUser(
user[index],
userInput[index],
filterPart,
`${path}.[${index}]`
pathToPayload.concat([{ type: "array", index: index }])
);
if (match) {

if (match.match) {
return match;
}
}
}

return null;
return {
match: false,
};
}

function getObjectWithOperators(
function removeKeysThatDontStartWithDollarSign(
filter: Record<string, unknown>
): Record<string, unknown> {
return Object.keys(filter).reduce((acc, key) => {
Expand All @@ -67,38 +97,45 @@ function getObjectWithOperators(
}

function findFilterPartWithOperators(
user: unknown,
userInput: unknown,
partOfFilter: unknown
): string | null {
): { found: false } | { found: true; pathToPayload: string } {
if (isPlainObject(partOfFilter)) {
const object = getObjectWithOperators(partOfFilter);
const object = removeKeysThatDontStartWithDollarSign(partOfFilter);
if (Object.keys(object).length > 0) {
const path = matchFilterPartInUser(user, object);
if (path) {
return path;
const result = matchFilterPartInUser(userInput, object);

if (result.match) {
return { found: true, pathToPayload: result.pathToPayload };
}
}

for (const key in partOfFilter) {
const path = findFilterPartWithOperators(user, partOfFilter[key]);
if (path) {
return path;
const result = findFilterPartWithOperators(userInput, partOfFilter[key]);

if (result.found) {
return { found: true, pathToPayload: result.pathToPayload };
}
}
}

if (Array.isArray(partOfFilter)) {
for (const value of partOfFilter) {
const path = findFilterPartWithOperators(user, value);
if (path) {
return path;
const result = findFilterPartWithOperators(userInput, value);

if (result.found) {
return { found: true, pathToPayload: result.pathToPayload };
}
}
}

return null;
return { found: false };
}

type DetectionResult =
| { injection: true; source: Source; pathToPayload: string }
| { injection: false };

export function detectNoSQLInjection(
request: Context,
filter: unknown
Expand All @@ -108,28 +145,14 @@ export function detectNoSQLInjection(
}

for (const source of ["body", "query", "headers", "cookies"] as Source[]) {
if (source === "body" && isPlainObject(request[source])) {
const object = getObjectWithOperators(filter);
if (
Object.keys(object).length > 0 &&
isDeepStrictEqual(request[source], object)
) {
return {
injection: true,
source: source,
path: ".",
};
}
}

if (request[source]) {
const path = findFilterPartWithOperators(request[source], filter);
const result = findFilterPartWithOperators(request[source], filter);

if (path) {
if (result.found) {
return {
injection: true,
source: source,
path: path,
pathToPayload: result.pathToPayload,
};
}
}
Expand Down

0 comments on commit 9a7b1fa

Please sign in to comment.