diff --git a/library/helpers/extractPathStringsFromUserInputCached.ts b/library/helpers/extractPathStringsFromUserInputCached.ts index 8136f1cad..94eb58cc3 100644 --- a/library/helpers/extractPathStringsFromUserInputCached.ts +++ b/library/helpers/extractPathStringsFromUserInputCached.ts @@ -1,7 +1,6 @@ import { Context } from "../agent/Context"; import { SOURCES } from "../agent/Source"; import { extractStringsFromUserInput } from "./extractStringsFromUserInput"; -import { sep } from "node:path"; type ReturnValue = ReturnType; @@ -23,7 +22,9 @@ export function extractPathStringsFromUserInputCached( // Performance optimization: only keep strings that contain a path separator // as only those can be used for path traversal // keeps the set smaller and speeds up `fs` and `path` operations - if (item.includes(sep)) { + // Check both separators: attackers may send backslash-only payloads on POSIX + // that the application normalizes to forward slashes before reaching the sink. + if (item.includes("/") || item.includes("\\")) { userStrings.add(item); } } diff --git a/library/tsconfig.json b/library/tsconfig.json index 0b0a799c2..ddc0502c5 100644 --- a/library/tsconfig.json +++ b/library/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "ES2019", - "lib": ["ES2019", "DOM"], + "target": "ES2021", + "lib": ["ES2021", "DOM"], "module": "commonjs", "moduleResolution": "node", "declaration": true, diff --git a/library/vulnerabilities/path-traversal/checkContextForPathTraversal.test.ts b/library/vulnerabilities/path-traversal/checkContextForPathTraversal.test.ts index b6c1a9357..a2cc3311b 100644 --- a/library/vulnerabilities/path-traversal/checkContextForPathTraversal.test.ts +++ b/library/vulnerabilities/path-traversal/checkContextForPathTraversal.test.ts @@ -20,6 +20,42 @@ const unsafeContext = { }, }; +t.test( + "it detects path traversal when attacker uses backslashes (POSIX bypass)", + async () => { + t.same( + checkContextForPathTraversal({ + filename: "../../../../etc/hosts", + operation: "path.resolve", + context: { + cookies: {}, + headers: {}, + remoteAddress: "ip", + method: "GET", + url: "url", + query: { + file: "..\\..\\..\\..\\etc\\hosts", + }, + body: {}, + routeParams: {}, + source: "express", + route: undefined, + }, + }), + { + operation: "path.resolve", + kind: "path_traversal", + source: "query", + pathsToPayload: [".file"], + metadata: { + filename: "../../../../etc/hosts", + }, + payload: "..\\..\\..\\..\\etc\\hosts", + } + ); + } +); + t.test("it detects path traversal from route parameter", async () => { t.same(checkContextForPathTraversal(unsafeContext), { operation: "operation", diff --git a/library/vulnerabilities/path-traversal/detectPathTraversal.ts b/library/vulnerabilities/path-traversal/detectPathTraversal.ts index f26ca40a4..550427b4e 100644 --- a/library/vulnerabilities/path-traversal/detectPathTraversal.ts +++ b/library/vulnerabilities/path-traversal/detectPathTraversal.ts @@ -29,13 +29,27 @@ export function detectPathTraversal( } } - if (userInput.length > filePath.length) { + // Normalize backslashes to forward slashes on both sides for cross-platform comparison. + // Attackers may send backslash payloads that the application normalizes to forward + // slashes before reaching the sink (filePath has '/', userInput still has '\'). + const normalizedFilePath = filePath.includes("\\") + ? filePath.replaceAll("\\", "/") + : filePath; + const normalizedUserInput = userInput.includes("\\") + ? userInput.replaceAll("\\", "/") + : userInput; + + if (normalizedUserInput.length > normalizedFilePath.length) { // We ignore cases where the user input is longer than the file path. // Because the user input can't be part of the file path. return false; } - if (!filePath.toLowerCase().includes(userInput.toLowerCase())) { + if ( + !normalizedFilePath + .toLowerCase() + .includes(normalizedUserInput.toLowerCase()) + ) { // We ignore cases where the user input is not part of the file path. return false; }