Skip to content

lab-ops-hub: container validator returns dot segments, host case and default ports un-normalized — fetcher requests a different path than the Worker #633

Closed

Description

@dezren39

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:

  1. 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.
  2. Host case is preserved. The Worker lowercases the host in the
    returned URL; the container returns EXAMPLE.COM as spelled.
  3. 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

Activity

  1. added a commit that references this issue on Oct 23, 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