Repository navigation
Description
Summary
WHATWG URL parsing for the special schemes (http/https) ignores the number of slashes after the scheme colon: it consumes the whole run of / and \ delimiters, and the next segment is the host. So http:example.com/x, http:/example.com/x, http:///example.com/x, http:////example.com/x and http:\\\example.com/x all name host example.com, and the Worker's validateCrawlUrl accepts and normalizes every one of them to http://example.com/x.
The container's validate_crawl_url only normalizes the exactly-two-delimiter spelling. #577 taught it to replace \ with / for http(s) inputs (pkgs/lab-ops-hub/container/app.py:76-77), which makes http:\\example.com/ parse — but urlparse still requires exactly // to see a netloc, so every other run length (0, 1, 3, 4, and mixed runs like http://\example.com/) parses with an empty netloc and is refused as url has no host.
Evidence (verified this run on current main, 22a4b98)
Differential probe, container validate_crawl_url vs Worker validateCrawlUrl run under Bun (the runtime the earlier parity issues used as reference; Node's URL parser differs — it accepts invalid xn-- A-labels that Bun and the container both reject, so Node is not the reference here). 70-spelling matrix over both schemes, run lengths 0–4 of /, \ and mixed, hosts example.com / 127.0.0.1 / localhost:
Container REFUSES (url has no host), Worker ACCEPTS and normalizes to http(s)://example.com/x:
http:example.com/x(no delimiter)http:/example.com/x(one slash)http:///example.com/x(three slashes)http:////example.com/x(four slashes)http:\example.com/x(one backslash)http:\\\example.com/x(three backslashes)http://\example.com/x(mixed run — the lab-ops-hub: container validator parses backslash authority differently from WHATWG (127.0.0.1\@example.com judged as example.com; example.com\@127.0.0.1 refused) #577\→/replacement turns this into four slashes, still no netloc to urlparse)- the same seven spellings under
https:
The exactly-two spellings (http://…, http:\\…, http:/\…) agree, which is why #577's tests pass. Blocked hosts in the divergent spellings are refused by both validators (container: no host; Worker: non-public / local host) — this is an over-refusal, not an SSRF bypass: no spelling was found where the container accepts a host the Worker refuses in this family.
Full differential otherwise clean: the same probe batches covering IPv4/IPv6 special ranges, A-labels, IDN/UTS-46 mapping, percent-encoded hosts, ports and userinfo show no other verdict differences under Bun.
Why it matters
The container re-validates independently as defence in depth, and for redirect hops it is the only validator: _ValidatingRedirectHandler.redirect_request calls validate_crawl_url(newurl) on the raw Location value (container/app.py, #529 evidence: "a redirect target is validated raw (no Worker normalisation in between)"). The Worker fallback instead resolves each Location with new URL(location, current) (src/lib.ts fetchFollowingValidatedRedirects), which applies the WHATWG slash-run rule. So a page that redirects to http:///public.example/ — or any 0/1/3+-slash spelling — crawls fine through the Worker fallback but fails through the container (502, redirect refused as hostless), purely depending on which crawler path serves the request. That is the same one-validator-certifies / one-refuses divergence shape as #502/#511/#531, in the over-refusal direction #531 already established as a bug class for this pair.
The main dispatch path is not affected (the Worker passes its normalized u.toString() URL to the container); the exposure is redirect hops and any direct container caller.
Acceptance criteria
validate_crawl_urljudges the host WHATWG names for any leading delimiter run after an http/https scheme colon: collapse the run of/(after the existing\→/normalization) followinghttp:/https:to exactly//beforeurlparse, so the seven spellings above (both schemes) validate — and are fetched — ashttp(s)://example.com/x.- Blocked targets in those spellings stay refused for the right reason:
http:///127.0.0.1/x,http:localhost/x,https:\\\192.168.0.1/xrefuse as non-public / local hosts, not as hostless URLs. - No verdict changes for any other spelling: the existing Python and TypeScript suites stay green, and a differential probe against the Worker validator (under Bun) over slash-run, IPv4/IPv6, A-label, IDN and percent-encoded batches shows no remaining differences in this family.
- Regression tests in
test/test_container_validate.pycover at least: one accepted public spelling per run class (0, 1, 3, mixed slashes/backslashes) and one refused blocked spelling.
Activity
Merge pull request #587 from developing-today/creator/586-slash-run-a…