Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions pkgs/lab-ops-hub/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,12 @@ export function validateCrawlUrl(raw: string): { ok: true; url: string } | { ok:
// container crawler validates every redirect target (container/app.py's
// _ValidatingRedirectHandler); the Worker fallback must hold the same
// policy. Follow redirects manually: resolve each Location against the
// current URL, validate it, and only then fetch it. The fetcher is injected
// (it must use redirect:"manual") so this is testable with plain Node.
// current URL, validate it, and only then fetch it. The start URL is
// validated the same way before the first fetch (#464): this helper is the
// component that actually fetches on the Worker-fallback path, so it must
// be safe by construction rather than rely on callers pre-validating. The
// fetcher is injected (it must use redirect:"manual") so this is testable
// with plain Node.

export type ManualFetcher = (url: string) => Promise<Response>;

Expand All @@ -107,7 +111,11 @@ export async function fetchFollowingValidatedRedirects(
fetchFn: ManualFetcher,
maxRedirects = 5,
): Promise<Response> {
let current = startUrl;
const start = validateCrawlUrl(startUrl);
if (!start.ok) {
throw new Error(`refusing to crawl ${startUrl}: ${start.error}`);
}
let current = start.url;
for (let hop = 0; ; hop++) {
const res = await fetchFn(current);
if (res.status < 300 || res.status >= 400) return res;
Expand Down
17 changes: 17 additions & 0 deletions pkgs/lab-ops-hub/test/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ test("fetchFollowingValidatedRedirects refuses redirects to non-public targets (
}
});

test("fetchFollowingValidatedRedirects refuses a non-public start URL without fetching (#464)", async () => {
for (const start of [
"http://127.0.0.1/secret",
"http://169.254.169.254/latest/meta-data/",
"http://2130706433/", // WHATWG-normalises to 127.0.0.1
"https://[::ffff:127.0.0.1]/", // IPv4-mapped loopback
"http://localhost:8787/",
"file:///etc/passwd",
"not a url",
]) {
const { fetchFn, seen } = stubFetcher({});
await assert.rejects(fetchFollowingValidatedRedirects(start, fetchFn), /refusing to crawl/, start);
// The start URL itself is never fetched.
assert.deepEqual(seen, [], start);
}
});

test("fetchFollowingValidatedRedirects bounds the hop count (#460)", async () => {
const { fetchFn } = stubFetcher({
"https://example.com/loop": { status: 302, location: "https://example.com/loop" },
Expand Down
Loading