Skip to content

Commit 35328bb

Browse files
authored
Merge pull request #592 from AikidoSec/path-traversal-bypass
Fix path traversal bypass
2 parents 62cdd1d + 444fdad commit 35328bb

File tree

6 files changed

+461
-1
lines changed

6 files changed

+461
-1
lines changed

library/helpers/extractStringsFromUserInput.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ t.test("it ignores iss value of jwt", async () => {
138138
t.test("it also adds the JWT itself as string", async () => {
139139
t.same(
140140
extractStringsFromUserInput({ header: "/;ping%20localhost;.e30=." }),
141-
fromArr(["header", "/;ping%20localhost;.e30=."])
141+
fromArr(["header", "/;ping%20localhost;.e30=.", "/;ping localhost;.e30=."])
142142
);
143143
});
144144

@@ -170,3 +170,22 @@ t.test("it concatenates array values", async () => {
170170
])
171171
);
172172
});
173+
174+
t.test("it decodes uri encoded strings", async () => {
175+
t.same(
176+
extractStringsFromUserInput({
177+
arr: ["1", "2", "3"],
178+
encoded: "%2E%2E/%2E%2Fetc%2Fpasswd",
179+
}),
180+
fromArr([
181+
"arr",
182+
"1",
183+
"2",
184+
"3",
185+
"1,2,3",
186+
"encoded",
187+
"%2E%2E/%2E%2Fetc%2Fpasswd",
188+
".././etc/passwd",
189+
])
190+
);
191+
});

library/helpers/extractStringsFromUserInput.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isPlainObject } from "./isPlainObject";
2+
import { safeDecodeURIComponent } from "./safeDecodeURIComponent";
23
import { tryDecodeAsJWT } from "./tryDecodeAsJWT";
34

45
type UserString = string;
@@ -31,6 +32,13 @@ export function extractStringsFromUserInput(obj: unknown): Set<UserString> {
3132
if (typeof obj == "string") {
3233
results.add(obj);
3334

35+
if (obj.includes("%") && obj.length >= 3) {
36+
const r = safeDecodeURIComponent(obj);
37+
if (r && r !== obj) {
38+
results.add(r);
39+
}
40+
}
41+
3442
const jwt = tryDecodeAsJWT(obj);
3543
if (jwt.jwt) {
3644
// Do not add the issuer of the JWT as a string because it can contain a domain / url and produce false positives
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import * as t from "tap";
2+
import { safeDecodeURIComponent } from "./safeDecodeURIComponent";
3+
4+
t.setTimeout(60000);
5+
6+
t.test("it decodes a URI component (static tests)", async (t) => {
7+
t.equal(safeDecodeURIComponent("%20"), " ");
8+
t.equal(safeDecodeURIComponent("%3A"), ":");
9+
t.equal(safeDecodeURIComponent("%2F"), "/");
10+
t.equal(safeDecodeURIComponent("%252F"), "%2F");
11+
t.equal(safeDecodeURIComponent("test%20test"), "test test");
12+
t.equal(safeDecodeURIComponent("test%3Atest"), "test:test");
13+
t.equal(safeDecodeURIComponent(encodeURIComponent("✨")), "✨");
14+
t.equal(safeDecodeURIComponent(encodeURIComponent("💜")), "💜");
15+
t.equal(safeDecodeURIComponent(encodeURIComponent("اللغة")), "اللغة");
16+
t.equal(safeDecodeURIComponent(encodeURIComponent("Γλώσσα")), "Γλώσσα");
17+
t.equal(safeDecodeURIComponent(encodeURIComponent("言語")), "言語");
18+
t.equal(safeDecodeURIComponent(encodeURIComponent("语言")), "语言");
19+
t.equal(safeDecodeURIComponent(encodeURIComponent("語言")), "語言");
20+
});
21+
22+
t.test("it returns undefined for invalid URI components", async (t) => {
23+
t.equal(safeDecodeURIComponent("%"), undefined);
24+
t.equal(safeDecodeURIComponent("%2"), undefined);
25+
t.equal(safeDecodeURIComponent("%2G"), undefined);
26+
t.equal(safeDecodeURIComponent("%2g"), undefined);
27+
t.equal(safeDecodeURIComponent("test%gtest"), undefined);
28+
t.equal(safeDecodeURIComponent("test%test"), undefined);
29+
t.equal(safeDecodeURIComponent("%99"), undefined);
30+
});
31+
32+
function generateRandomTestString(
33+
length = Math.floor(Math.random() * 100) + 1
34+
) {
35+
let result = "";
36+
const characters =
37+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-_~%+";
38+
for (let i = 0; i < length; i++) {
39+
result += characters.charAt(Math.floor(Math.random() * characters.length));
40+
}
41+
return result;
42+
}
43+
44+
const testCases = [
45+
"test",
46+
"42",
47+
"a+b+c+d",
48+
"=a",
49+
"%25",
50+
"%%25%%",
51+
"st%C3%A5le",
52+
"st%C3%A5le%",
53+
"%st%C3%A5le%",
54+
"%%7Bst%C3%A5le%7D%",
55+
"%ab%C3%A5le%",
56+
"%C3%A5%able%",
57+
"%7B%ab%7C%de%7D",
58+
"%7B%ab%%7C%de%%7D",
59+
"%7 B%ab%%7C%de%%7 D",
60+
"%61+%4d%4D",
61+
"\uFEFFtest",
62+
"\uFEFF",
63+
"%EF%BB%BFtest",
64+
"%EF%BB%BF",
65+
"†",
66+
"%C2%B5",
67+
"%C2%B5%",
68+
"%%C2%B5%",
69+
"%ab",
70+
"%ab%ab%ab",
71+
"%",
72+
"%2",
73+
"%E0%A4%A",
74+
'/test/hel%"Flo',
75+
"/test/hel%2Flo",
76+
"%E8%AF%AD%E8%A8%80",
77+
"%",
78+
"%2",
79+
"%2G",
80+
"%2g",
81+
"%2g%2g",
82+
"test%2gtest",
83+
"test%test",
84+
"%99",
85+
];
86+
87+
const randomTestCases = 10_000;
88+
const randomStrings = Array.from({ length: randomTestCases }, () =>
89+
generateRandomTestString()
90+
);
91+
92+
const allTestCases = [...testCases, ...randomStrings];
93+
94+
t.test("compare with original decodeURIComponent", async (t) => {
95+
for (const testCase of allTestCases) {
96+
let origResult = undefined;
97+
try {
98+
origResult = decodeURIComponent(testCase);
99+
} catch {
100+
//
101+
}
102+
t.equal(safeDecodeURIComponent(testCase), origResult);
103+
}
104+
});
105+
106+
t.test("benchmark", async (t) => {
107+
const startOrig = performance.now();
108+
for (const testCase of allTestCases) {
109+
try {
110+
decodeURIComponent(testCase);
111+
} catch {
112+
//
113+
}
114+
}
115+
const endOrig = performance.now();
116+
117+
const startSafe = performance.now();
118+
for (const testCase of allTestCases) {
119+
safeDecodeURIComponent(testCase);
120+
}
121+
const endSafe = performance.now();
122+
123+
const origDuration = endOrig - startOrig;
124+
const safeDuration = endSafe - startSafe;
125+
t.ok(
126+
safeDuration < origDuration,
127+
`safeDecodeURIComponent is faster than decodeURIComponent`
128+
);
129+
130+
const origSpeedup = (origDuration - safeDuration) / origDuration;
131+
t.ok(
132+
origSpeedup > 0.7,
133+
`safeDecodeURIComponent is at least 70% faster than decodeURIComponent`
134+
);
135+
t.comment(`Perdormance improvement: ${origSpeedup * 100}%`);
136+
});
137+
138+
// The following tests are ported from test262, the Official ECMAScript Conformance Test Suite
139+
// https://github.com/tc39/test262
140+
// Licensed under the MIT License
141+
// Copyright (C) 2012 Ecma International
142+
143+
t.test("S15.1.3.2_A1.10_T1", async (t) => {
144+
const interval = [
145+
[0x00, 0x2f],
146+
[0x3a, 0x40],
147+
[0x47, 0x60],
148+
[0x67, 0xffff],
149+
];
150+
for (let indexI = 0; indexI < interval.length; indexI++) {
151+
for (
152+
let indexJ = interval[indexI][0];
153+
indexJ <= interval[indexI][1];
154+
indexJ++
155+
) {
156+
t.equal(
157+
safeDecodeURIComponent("%C0%" + String.fromCharCode(indexJ, indexJ)),
158+
undefined
159+
);
160+
}
161+
}
162+
});
163+
164+
t.test("S15.1.3.2_A1.11_T1", async (t) => {
165+
const interval = [
166+
[0x00, 0x2f],
167+
[0x3a, 0x40],
168+
[0x47, 0x60],
169+
[0x67, 0xffff],
170+
];
171+
for (let indexI = 0; indexI < interval.length; indexI++) {
172+
for (
173+
let indexJ = interval[indexI][0];
174+
indexJ <= interval[indexI][1];
175+
indexJ++
176+
) {
177+
t.equal(
178+
safeDecodeURIComponent(
179+
"%E0%" + String.fromCharCode(indexJ, indexJ) + "%A0"
180+
),
181+
undefined
182+
);
183+
}
184+
}
185+
});
186+
187+
t.test("S15.1.3.2_A1.1_T1", async (t) => {
188+
t.equal(safeDecodeURIComponent("%"), undefined);
189+
t.equal(safeDecodeURIComponent("%A"), undefined);
190+
t.equal(safeDecodeURIComponent("%1"), undefined);
191+
t.equal(safeDecodeURIComponent("% "), undefined);
192+
});
193+
194+
t.test("S15.1.3.2_A3_T1", async (t) => {
195+
t.equal(safeDecodeURIComponent("%3B"), ";");
196+
t.equal(safeDecodeURIComponent("%2F"), "/");
197+
t.equal(safeDecodeURIComponent("%3F"), "?");
198+
t.equal(safeDecodeURIComponent("%3A"), ":");
199+
t.equal(safeDecodeURIComponent("%40"), "@");
200+
t.equal(safeDecodeURIComponent("%26"), "&");
201+
t.equal(safeDecodeURIComponent("%3D"), "=");
202+
t.equal(safeDecodeURIComponent("%2B"), "+");
203+
t.equal(safeDecodeURIComponent("%24"), "$");
204+
t.equal(safeDecodeURIComponent("%2C"), ",");
205+
t.equal(safeDecodeURIComponent("%23"), "#");
206+
});
207+
208+
t.test("S15.1.3.2_A3_T2", async (t) => {
209+
t.equal(safeDecodeURIComponent("%3b"), ";");
210+
t.equal(safeDecodeURIComponent("%2f"), "/");
211+
t.equal(safeDecodeURIComponent("%3f"), "?");
212+
t.equal(safeDecodeURIComponent("%3a"), ":");
213+
t.equal(safeDecodeURIComponent("%40"), "@");
214+
t.equal(safeDecodeURIComponent("%26"), "&");
215+
t.equal(safeDecodeURIComponent("%3d"), "=");
216+
t.equal(safeDecodeURIComponent("%2b"), "+");
217+
t.equal(safeDecodeURIComponent("%24"), "$");
218+
t.equal(safeDecodeURIComponent("%2c"), ",");
219+
t.equal(safeDecodeURIComponent("%23"), "#");
220+
});
221+
222+
t.test("S15.1.3.2_A3_T3", async (t) => {
223+
t.equal(
224+
safeDecodeURIComponent("%3B%2F%3F%3A%40%26%3D%2B%24%2C%23"),
225+
";/?:@&=+$,#"
226+
);
227+
t.equal(
228+
safeDecodeURIComponent("%3b%2f%3f%3a%40%26%3d%2b%24%2c%23"),
229+
";/?:@&=+$,#"
230+
);
231+
});
232+
233+
t.test("S15.1.3.2_A4_T1", async (t) => {
234+
t.equal(
235+
safeDecodeURIComponent(
236+
"%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A"
237+
),
238+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
239+
);
240+
t.equal(
241+
safeDecodeURIComponent(
242+
"%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A"
243+
),
244+
"abcdefghijklmnopqrstuvwxyz"
245+
);
246+
});

0 commit comments

Comments
 (0)