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
8 changes: 7 additions & 1 deletion pkgs/lab-ops-hub/container/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ def validate_crawl_url(raw):
# refuse it instead of reclassifying it as a public DNS
# name, matching the Worker's validator (#502).
last_label = host.rsplit(".", 1)[-1]
if re.fullmatch(r"0[xX][0-9a-fA-F]+|[0-9]+", last_label):
# The hex alternative allows zero digits: WHATWG's
# IPv4-number parser returns 0 for a bare 0x/0X prefix, so
# `example.0x` and the single-label `0x` (0.0.0.0) also end
# in a number — refuse them too (#504). A trailing non-hex
# character (0xg, 0x7fg) still fails the full match and
# stays a DNS name, as in WHATWG.
if re.fullmatch(r"0[xX][0-9a-fA-F]*|[0-9]+", last_label):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve valid public IPv4 forms ending in bare 0x

When a redirect uses an all-numeric host such as http://8.8.8.0x/, WHATWG parses the bare final component as zero and the Worker normalizes and accepts it as public 8.8.8.0. Python's socket.inet_aton does not recognize that spelling, so this new unconditional final-label match instead rejects it, causing the container crawl to return 502 and unnecessarily fall back to the Worker. Distinguish malformed names such as example.0x from valid numeric IPv4 components before rejecting the host.

Useful? React with 👍 / 👎.

raise ValueError(
f"refusing to crawl host {host}: numeric final label "
"but not a valid IPv4 address"
Expand Down
23 changes: 23 additions & 0 deletions pkgs/lab-ops-hub/test/test_container_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ def test_refuses_numeric_final_label_hosts_that_are_not_ipv4(self):
):
self.assertRefused(url)

def test_refuses_bare_0x_final_label(self):
# WHATWG's IPv4-number parser returns 0 for a bare 0x/0X prefix
# (the digit sequence after it is empty), so these hosts also
# end in a number: `0x` alone normalises to 0.0.0.0 and the
# Worker's validator refuses every spelling, while the #502
# pattern required at least one hex digit after the prefix
# (#504). Redirect targets inherit via the same function.
for url in (
"http://example.0x/",
"http://example.0X/",
"http://example.0x./", # trailing root dot stripped first
"http://0x/",
"http://0X/",
):
self.assertRefused(url)
# A trailing non-hex character makes the label not an IPv4
# number in WHATWG either, so these stay public DNS names.
for url in (
"http://example.0xg/",
"http://example.0x7fg/",
):
self.assertAccepted(url)

def test_refuses_malformed_or_out_of_range_ports(self):
# urlparse reports these only lazily via .port; the validator
# must refuse them up front like the Worker's URL parser (#502).
Expand Down
Loading