Skip to content

Commit 916e44a

Browse files
authored
Merge pull request #581 from AikidoSec/fix-url-path-traversal-bypass
Fix url path traversal bypass
2 parents 9a17217 + 1d0b9fb commit 916e44a

File tree

5 files changed

+191
-2
lines changed

5 files changed

+191
-2
lines changed

library/sinks/FileSystem.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,82 @@ t.test("it works", async (t) => {
201201
);
202202
});
203203

204+
runWithContext(
205+
{
206+
remoteAddress: "::1",
207+
method: "POST",
208+
url: "http://localhost:4000",
209+
query: {
210+
q: ".\t./etc/passwd",
211+
},
212+
headers: {},
213+
body: {},
214+
cookies: {},
215+
routeParams: {},
216+
source: "express",
217+
route: "/posts/:id",
218+
},
219+
() => {
220+
throws(
221+
() =>
222+
rename(
223+
new URL("file:///.\t./etc/passwd"),
224+
"../test123.txt",
225+
() => {}
226+
),
227+
"Zen has blocked a path traversal attack: fs.rename(...) originating from query.q"
228+
);
229+
}
230+
);
231+
232+
runWithContext(
233+
{
234+
remoteAddress: "::1",
235+
method: "POST",
236+
url: "http://localhost:4000",
237+
query: {
238+
q: "test/test.txt",
239+
},
240+
headers: {},
241+
body: {},
242+
cookies: {},
243+
routeParams: {},
244+
source: "express",
245+
route: "/posts/:id",
246+
},
247+
() => {
248+
rename(new URL("file:///test/test.txt"), "../test123.txt", () => {});
249+
}
250+
);
251+
252+
runWithContext(
253+
{
254+
remoteAddress: "::1",
255+
method: "POST",
256+
url: "http://localhost:4000",
257+
query: {
258+
q: ".\t\t./etc/passwd",
259+
},
260+
headers: {},
261+
body: {},
262+
cookies: {},
263+
routeParams: {},
264+
source: "express",
265+
route: "/posts/:id",
266+
},
267+
() => {
268+
throws(
269+
() =>
270+
rename(
271+
new URL("file:///.\t\t./etc/passwd"),
272+
"../test123.txt",
273+
() => {}
274+
),
275+
"Zen has blocked a path traversal attack: fs.rename(...) originating from query.q"
276+
);
277+
}
278+
);
279+
204280
// Ignores malformed URLs
205281
runWithContext(
206282
{ ...unsafeContext, body: { file: { matches: "../%" } } },

library/vulnerabilities/path-traversal/checkContextForPathTraversal.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,40 @@ t.test("it ignores invalid filename type", async () => {
138138
undefined
139139
);
140140
});
141+
142+
t.test("it works with control characters removed by new URL", async (t) => {
143+
const queries = [".\t./etc/passwd", ".\n./etc/passwd", ".\r./etc/passwd"];
144+
145+
for (const query of queries) {
146+
t.same(
147+
checkContextForPathTraversal({
148+
filename: new URL(`file:///test/${query}`),
149+
operation: "operation",
150+
context: {
151+
cookies: {},
152+
headers: {},
153+
remoteAddress: "ip",
154+
method: "POST",
155+
url: "url",
156+
query: {
157+
q: query,
158+
},
159+
body: {},
160+
routeParams: {},
161+
source: "express",
162+
route: undefined,
163+
},
164+
}),
165+
{
166+
operation: "operation",
167+
kind: "path_traversal",
168+
source: "query",
169+
pathsToPayload: [".q"],
170+
metadata: {
171+
filename: "/etc/passwd",
172+
},
173+
payload: query,
174+
}
175+
);
176+
}
177+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as t from "tap";
2+
import {
3+
containsUnsafePathParts,
4+
containsUnsafePathPartsUrl,
5+
} from "./containsUnsafePathParts";
6+
import { fileURLToPath } from "url";
7+
8+
t.test("not a dangerous path", async () => {
9+
t.same(containsUnsafePathParts("test.txt"), false);
10+
t.same(containsUnsafePathParts("/var/www/test.txt"), false);
11+
t.same(containsUnsafePathParts("./test.txt"), false);
12+
t.same(containsUnsafePathParts("test.txt/"), false);
13+
});
14+
15+
t.test("it detects dangerous path parts", async () => {
16+
t.same(containsUnsafePathParts("../test.txt"), true);
17+
t.same(containsUnsafePathParts("..\\test.txt"), true);
18+
t.same(containsUnsafePathParts("../../test.txt"), true);
19+
t.same(containsUnsafePathParts("..\\..\\test.txt"), true);
20+
t.same(containsUnsafePathParts("../../../../test.txt"), true);
21+
t.same(containsUnsafePathParts("..\\..\\..\\..\\test.txt"), true);
22+
t.same(containsUnsafePathParts("/test/../test.txt"), true);
23+
t.same(containsUnsafePathParts("/test/..\\test.txt"), true);
24+
});
25+
26+
t.test(
27+
"containsUnsafePathParts does not detect with control characters",
28+
async () => {
29+
t.same(containsUnsafePathParts("file:///.\t./test.txt"), false);
30+
t.same(containsUnsafePathParts("file:///.\n./test.txt"), false);
31+
t.same(containsUnsafePathParts("file:///.\r./test.txt"), false);
32+
}
33+
);
34+
35+
t.test("it detects dangerous path parts for URLs", async () => {
36+
t.same(containsUnsafePathPartsUrl("/../test.txt"), true);
37+
t.same(containsUnsafePathPartsUrl("/..\\test.txt"), true);
38+
t.same(containsUnsafePathPartsUrl("file:///../test.txt"), true);
39+
t.same(containsUnsafePathPartsUrl("file://..\\test.txt"), true);
40+
t.same(containsUnsafePathPartsUrl("file:///.\t./test.txt"), true);
41+
t.same(containsUnsafePathPartsUrl("file://.\n./test.txt"), true);
42+
t.same(containsUnsafePathPartsUrl("file://.\r./test.txt"), true);
43+
t.same(containsUnsafePathPartsUrl("file:///.\t\t./test.txt"), true);
44+
t.same(containsUnsafePathPartsUrl("file:///.\t\n./test.txt"), true);
45+
});
46+
47+
t.test("it only removes some chars from the URL", async () => {
48+
t.same(fileURLToPath("file:///.\t./test.txt"), "/test.txt");
49+
t.same(fileURLToPath("file:///.\n./test.txt"), "/test.txt");
50+
t.same(fileURLToPath("file:///.\r./test.txt"), "/test.txt");
51+
t.same(fileURLToPath("file:///.\0./test.txt"), "/.\0./test.txt");
52+
t.same(fileURLToPath("file:///.\u0000./test.txt"), "/.\u0000./test.txt");
53+
t.same(fileURLToPath("file:///.\v./test.txt"), "/.\v./test.txt");
54+
t.same(fileURLToPath("file:///.\f./test.txt"), "/.\f./test.txt");
55+
t.same(fileURLToPath("file:///.\b./test.txt"), "/.\b./test.txt");
56+
t.same(fileURLToPath("file:///.\t\t./test.txt"), "/test.txt");
57+
t.same(fileURLToPath("file:///.\t\n./test.txt"), "/test.txt");
58+
});

library/vulnerabilities/path-traversal/containsUnsafePathParts.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,17 @@ export function containsUnsafePathParts(filePath: string) {
99

1010
return false;
1111
}
12+
13+
/**
14+
* This function is used for urls, because they can contain a TAB, carriage return or line feed that is silently removed by the URL constructor.
15+
*
16+
* The WHATWG URL spec defines the following:
17+
* - Remove all ASCII tab or newline from input.
18+
* - An ASCII tab or newline is U+0009 TAB, U+000A LF, or U+000D CR.
19+
*
20+
* See https://url.spec.whatwg.org/#url-parsing
21+
*/
22+
export function containsUnsafePathPartsUrl(filePath: string) {
23+
const normalized = filePath.replace(/[\t\n\r]/g, "");
24+
return containsUnsafePathParts(normalized);
25+
}

library/vulnerabilities/path-traversal/detectPathTraversal.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { containsUnsafePathParts } from "./containsUnsafePathParts";
1+
import {
2+
containsUnsafePathParts,
3+
containsUnsafePathPartsUrl,
4+
} from "./containsUnsafePathParts";
25
import { startsWithUnsafePath } from "./unsafePathStart";
36
import { fileURLToPath } from "url";
47

@@ -18,7 +21,8 @@ export function detectPathTraversal(
1821
// The normal check for relative path traversal will fail in this case, because transformed path does not contain ../.
1922
// For absolute path traversal, we dont need to check the transformed path, because it will always start with /.
2023
// Also /./ is checked by normal absolute path traversal check (if #219 is merged)
21-
if (isUrl && containsUnsafePathParts(userInput)) {
24+
// Use containsUnsafePathPartsUrl, because urls can contain a TAB, carriage return or line feed that is silently removed by the URL constructor.
25+
if (isUrl && containsUnsafePathPartsUrl(userInput)) {
2226
const filePathFromUrl = parseAsFileUrl(userInput);
2327
if (filePathFromUrl && filePath.includes(filePathFromUrl)) {
2428
return true;

0 commit comments

Comments
 (0)