Skip to content

lab-ops-hub crawl validators disagree on IPv4 ranges — Python allows multicast, TS over-blocks public /24 neighbours as /16s #476

Closed

Description

@dezren39

Problem

The two lab-ops-hub crawl validators claim the same policy (pkgs/lab-ops-hub/src/lib.ts: "The Python container crawler applies the same policy independently"; pkgs/lab-ops-hub/container/app.py: "Only public http(s) targets"), but they disagree on IPv4 literals. Verified this run on main 668dd32f by calling both validators on the same 2,542 candidate URLs (special-range + boundary samples): 169 divergent verdicts, all TS blocks / Python allows.

1. Python container validator ALLOWS IPv4 multicast — TS blocks it

Python's plain-IPv4 path is just elif not ip.is_global: raise, and ipaddress reports is_global == True for multicast (verified: 224.0.0.1 → is_global True, is_multicast True):

URL Python (container/app.py validate_crawl_url) TS (src/lib.ts validateCrawlUrl)
http://224.0.0.1/ … http://239.255.0.1/ (224.0.0.0/4) ALLOW BLOCK (a >= 224 in ipv4IsGlobal)

The container is the component that actually fetches. The same flaw reaches the embedded-IPv4 path: _embedded_ipv4 verdicts are judged with embedded.is_global, so e.g. an IPv4-mapped/NAT64 literal embedding a multicast address is also allowed. This is the IPv4 analogue of #474 (which fixed the IPv6 case by requiring global unicast 2000::/3 on the Python side — no equivalent guard exists for IPv4).

2. TS Worker validator over-blocks four public ranges as /16s instead of /24s

ipv4IsGlobal in src/lib.ts:26-41 keys the narrow special ranges on the second octet (b), so a /24 whose distinguishing octet is the third is widened to a /16 — contradicting its own comments:

Code Comment says Actually blocks Public addresses wrongly refused (Python: ALLOW)
a===192 && b===0 192.0.0.0/24 + TEST-NET-1 192.0.2.0/24 192.0.0.0/16 192.0.1.1; also the global 192.0.0.9/192.0.0.10 exceptions inside the /24
a===192 && b===88 6to4 relay anycast (192.88.99.0/24) 192.88.0.0/16 192.88.0.1, 192.88.1.1
a===198 && b===51 TEST-NET-2 (198.51.100.0/24) 198.51.0.0/16 198.51.0.1, 198.51.1.1
a===203 && b===0 TEST-NET-3 (203.0.113.0/24) 203.0.0.0/16 203.0.0.1, 203.0.1.1

Note the reverse gap inside the 6to4 row: Python allows 192.88.99.1 (is_global True), so the deprecated relay-anycast /24 the TS comment intends to refuse is only refused on the Worker path.

Because TS ipv4IsGlobal is reused for embedded IPv4 (IPv4-mapped, NAT64, 6to4 in ipv6IsGlobal), the over-blocks propagate to those IPv6 spellings too.

Why it matters

The validators are defence-in-depth for one SSRF policy (#441, #460–#474). The fetching component accepting the whole multicast range defeats the policy; the Worker refusing ordinary public addresses in four /16s is a correctness bug that also makes the two implementations impossible to keep in sync with shared test vectors.

Acceptance criteria

Activity

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