Public: anyone can read this; the crew can change it.

A test pins the open-redirect guard: `isLocalPath` and a form's `back` refuse every off-origin form #6630

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6620-a-test-pins-the-open-redirect-guard-islo → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/6620-a-test-pins-the-open-redirect-guard-islo was merged into main as 4f7e4d3.

Review

Sign in to review this change.
1 file changed +62 −0
test/local-path.test.ts added +62
@@ -0,0 +1,62 @@
1 import { expect, test } from "bun:test";
2 import { isLocalPath } from "../src/web/flash.ts";
3 import { backTo, type Form } from "../src/web/form.ts";
4
5 const refused: [string, string][] = [
6 ["protocol-relative", "//evil.example"],
7 ["backslash after slash", "/\\evil.example"],
8 ["absolute URL", "https://evil.example"],
9 ["javascript scheme", "javascript:alert(1)"],
10 ["bare host", "evil.example/x"],
11 ["empty", ""],
12 ["tab", "/a\tb"],
13 ["newline", "/a\nb"],
14 ["NUL", "/a\u0000b"],
15 ["backslash inside", "/a/b\\c"],
16 ["trailing backslash", "/a\\"],
17 ["leading space", " /a"],
18 ["513 characters", "/" + "a".repeat(512)],
19 ];
20
21 const accepted: [string, string][] = [
22 ["root", "/"],
23 ["query and fragment", "/a/b?c=d#e"],
24 ["encoded slashes", "/%2F%2Fx"],
25 ["encoded backslash (the guard does not decode)", "/%5Cevil"],
26 ["512 characters", "/" + "a".repeat(511)],
27 ];
28

Good pin: documenting that the guard does not decode percent-escapes means a future decode step has to change this test on purpose.

29 function formWith(back: string): Form {
30 const form = new FormData();
31 form.set("back", back);
32 return form as unknown as Form;
33 }
34
35 test("the guard refuses every off-origin value", () => {
36 for (const [name, value] of refused)
37 expect([name, isLocalPath(value)]).toEqual([name, false]);
38 });
39
40 test("the guard accepts same-origin paths", () => {
41 for (const [name, value] of accepted)
42 expect([name, isLocalPath(value)]).toEqual([name, true]);
43 });
44
45 test("backTo returns the fallback for each refused value", () => {
46 for (const [name, value] of refused)
47 expect([name, backTo(formWith(value), "/fallback")]).toEqual([
48 name,
49 "/fallback",
50 ]);
51 });
52
53 test("backTo returns each accepted value itself", () => {
54 for (const [name, value] of accepted)
55 expect([name, backTo(formWith(value), "/fallback")]).toEqual([name, value]);
56 });
57
58 test("backTo returns the fallback when the form has no back field", () => {
59 expect(backTo(new FormData() as unknown as Form, "/fallback")).toBe(
60 "/fallback",
61 );
62 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs