Skip to content

lab-ops-hub: container validator refuses slash-run authority spellings the Worker accepts — http:host, http:/host, http:///host (only exactly-two delimiters normalized by #577) #586

Closed

Description

@dezren39

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:

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_url judges the host WHATWG names for any leading delimiter run after an http/https scheme colon: collapse the run of / (after the existing \→/ normalization) following http: / https: to exactly // before urlparse, so the seven spellings above (both schemes) validate — and are fetched — as http(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/x refuse 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.py cover at least: one accepted public spelling per run class (0, 1, 3, mixed slashes/backslashes) and one refused blocked spelling.

Activity

  1. added a commit that references this issue on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions