Repository navigation
Description
Finding
The container validator returns the crawl target without the WHATWG
normalizations the Worker validator applies, so for the same accepted
target the two crawlers record — and fetch — different URLs:
- Dot segments are never resolved. Literal
./..segments and
their percent-encoded spellings (%2e,.%2e,%2e.,%2e%2e, any
case) are returned verbatim by the container and sent verbatim on the
wire by its fetcher. The Worker resolves them per the WHATWG URL path
rules before fetching. - Host case is preserved. The Worker lowercases the host in the
returned URL; the container returnsEXAMPLE.COMas spelled. - Default ports are preserved, including leading-zero spellings:
the Worker drops:80/:443(and:080/:0080); the container
returns them verbatim.
validate_crawl_url (pkgs/lab-ops-hub/container/app.py:77, returns at
:399 and :439) returns _url_with_encoded_components(raw …) — the raw
spelling with only the character-encoding fixes of #614/#615/#618. No
step resolves path segments, lowercases the host, or drops a default
port. The Worker validator (pkgs/lab-ops-hub/src/lib.ts:130) returns
u.toString() (:182, :188), the fully WHATWG-normalized form.
Evidence
Verified this run on main 00bb59d5 (lab-ops-hub at 07a16b76),
driving both real validators (Python validate_crawl_url; Worker's
validateCrawlUrl under Bun):
| Input | Container returns | Worker returns |
|---|---|---|
http://example.com/a/../b |
http://example.com/a/../b |
http://example.com/b |
http://example.com/a/./b |
http://example.com/a/./b |
http://example.com/a/b |
http://example.com/%2e%2e/admin |
http://example.com/%2e%2e/admin |
http://example.com/admin |
http://example.com/a/%2e%2e/b |
http://example.com/a/%2e%2e/b |
http://example.com/b |
http://example.com/%2e/%2e/x |
http://example.com/%2e/%2e/x |
http://example.com/x |
http://example.com/a/%2E/b |
http://example.com/a/%2E/b |
http://example.com/a/b |
HTTP://EXAMPLE.COM/Path |
http://EXAMPLE.COM/Path |
http://example.com/Path |
http://example.com:80/x |
http://example.com:80/x |
http://example.com/x |
https://example.com:443/x |
https://example.com:443/x |
https://example.com/x |
http://example.com:080/x |
http://example.com:080/x |
http://example.com/x |
http://user:pass@EXAMPLE.com:80/ |
http://user:pass@EXAMPLE.com:80/ |
http://user:pass@example.com/ |
Fetcher consequence, reproduced with the app's own _fetch_crawl
(app.py:827) against a local HTTP server (proxy env unset): for
http://127.0.0.1:<port>/a/../b the server receives the request target
/a/../b verbatim, and for /%2e%2e/admin it receives /%2e%2e/admin
verbatim. The Worker's WHATWG fetch requests /b and /admin. An
origin (or any proxy/WAF in front of it) that routes on the raw request
target therefore serves the container a different resource than the
Worker for the same validated target, and %2e%2e spellings are the
classic path-confusion form when one layer resolves them and another
does not. Redirect targets inherit the divergence: the container's
_ValidatingRedirectHandler validates the raw Location value and
fetches the container's returned form.
Verdicts agree throughout (every spelling above is accepted by both
validators, and blocked-host controls — LOCALHOST, 127.0.0.1:080 —
are refused by both): this is a fetched-URL identity divergence, not an
accept/refuse one. Minor opposite-direction note from the same battery:
empty query/fragment markers — the Worker keeps a trailing ? / #
(http://example.com/x?), the container drops them; folding that into
the same normalization pass settles it either way.
Dedup: searches for "dot segment", "default port", "lowercase host"
and "%2e" return no issue covering this; the closed #614/#615/#618
series covers character encoding on the returned URL only. The
validator pair exists only in this repo (grep of the
opencode/openchamber/nix clones is empty).
Why it matters
The container and the Worker fallback are supposed to crawl the same
target; the last several fixes in this area (#577/#586/#611/#614/#615/
#618) each aligned one piece of the returned URL so "the URL the
fetcher fetches" is the URL the Worker fetches. Path identity is the
remaining piece: today a target spelled with dot segments — including
the percent-encoded traversal spelling — is fetched as two different
paths depending on which crawler serves it, so crawl results (status,
body, recorded URL) differ by crawler for reasons invisible to callers.
Acceptance criteria
validate_crawl_urlreturns the WHATWG-normalized form for the
battery above: dot segments (literal and%2espellings, any case,
including a trailing dot segment's trailing-slash semantics) resolved,
host lowercased, and the scheme's default port dropped (including
leading-zero spellings); non-default ports, empty path segments
(//), query and fragment are preserved as WHATWG preserves them.- A fetcher end-to-end test shows the container requesting the resolved
path (/b,/admin) for dot-segment targets, matching the Worker. - A container-vs-Worker differential over the battery (plus the
existing validator batteries) shows byte-identical returned URLs,
with the existing Python and TS suites still passing. - Care with ordering: normalization must not double-decode
percent-encodings the lab-ops-hub: container validator blanket-refuses percent-encoded hosts the Worker validator decodes and accepts — public spellings (%65xample.com, 8%2e8%2e8%2e8) diverge, including as redirect targets #529/lab-ops-hub: container validator returns non-ASCII path/query/userinfo verbatim — fetcher raises UnicodeEncodeError on URLs both validators accept #615/lab-ops-hub: container validator returns WHATWG-encodable ASCII punctuation verbatim outside the host — path/query/fragment/userinfo " < > ^ ` { } go on the request line raw while the Worker fetches the encoded form #618 steps produced (e.g. a literal
%252esegment is data, not a dot segment).
Activity
Merge pull request #644 from developing-today/sweep/633-dotseg-hostca…