Skip to content

Commit 4e12de1

Browse files
committed
fix: reject non-https schemes in SSRF check, expand tests
Add protocol check to assertGitHubAPIUrl so blob: URLs (whose origin matches api.github.com) are rejected. Add positive test for valid URL, pagination URL validation test, and blob:/data: scheme rejection tests.
1 parent ed82db5 commit 4e12de1

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

routes/github/lib/utils/rest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function tryURL(url: string) {
1212

1313
function assertGitHubAPIUrl(url: string) {
1414
const parsed = tryURL(url);
15-
if (parsed?.origin !== GITHUB_API_ORIGIN) {
15+
if (parsed?.protocol !== "https:" || parsed.origin !== GITHUB_API_ORIGIN) {
1616
throw new Error(
1717
`requestData: endpoint origin must be ${GITHUB_API_ORIGIN}, got ${parsed?.origin ?? "invalid URL"}`,
1818
);

tests/routes/github/lib/utils/rest.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,59 @@ describe("routes/github/lib/utils/rest - requestData", () => {
3535
/endpoint origin must be https:\/\/api\.github\.com/,
3636
);
3737
});
38+
39+
it("throws for a blob: URL with matching origin", async () => {
40+
const gen = requestData("blob:https://api.github.com/some-uuid");
41+
await expectAsync(gen.next()).toBeRejectedWithError(
42+
/endpoint origin must be https:\/\/api\.github\.com/,
43+
);
44+
});
45+
46+
it("throws for a data: URL", async () => {
47+
const gen = requestData("data:text/html,<h1>hi</h1>");
48+
await expectAsync(gen.next()).toBeRejectedWithError(
49+
/endpoint origin must be https:\/\/api\.github\.com/,
50+
);
51+
});
52+
53+
it("accepts a valid GitHub API URL", async () => {
54+
const original = globalThis.fetch;
55+
globalThis.fetch = async () => new Response(JSON.stringify([]), {
56+
status: 200,
57+
headers: {
58+
"x-ratelimit-remaining": "10",
59+
"x-ratelimit-reset": "9999999999",
60+
"x-ratelimit-limit": "60",
61+
},
62+
});
63+
try {
64+
const gen = requestData("https://api.github.com/repos/w3c/respec/issues");
65+
const { value } = await gen.next();
66+
expect(value.url).toBe("https://api.github.com/repos/w3c/respec/issues");
67+
} finally {
68+
globalThis.fetch = original;
69+
}
70+
});
71+
72+
it("rejects a malicious pagination URL in Link header", async () => {
73+
const original = globalThis.fetch;
74+
globalThis.fetch = async () => new Response(JSON.stringify([]), {
75+
status: 200,
76+
headers: {
77+
link: '<https://evil.com/page2>; rel="next"',
78+
"x-ratelimit-remaining": "10",
79+
"x-ratelimit-reset": "9999999999",
80+
"x-ratelimit-limit": "60",
81+
},
82+
});
83+
try {
84+
const gen = requestData("https://api.github.com/repos/w3c/respec/issues");
85+
await gen.next();
86+
await expectAsync(gen.next()).toBeRejectedWithError(
87+
/endpoint origin must be https:\/\/api\.github\.com/,
88+
);
89+
} finally {
90+
globalThis.fetch = original;
91+
}
92+
});
3893
});

0 commit comments

Comments
 (0)