GHSA-h47f-gmjp-m7rr compliance-trestle has an URLSecurityValidator SSRF allowlist bypass via IPv4-mapped IPv6 and 0.0.0.0
Summary`compliance-trestle` 4.0.3 (latest) ships an `URLSecurityValidator` in `trestle/core/remote/security.py` to block SSRF to loopback / link-local / cloud-metadata endpoints from the HTTPSFetcher and SFTPFetcher remote-fetch paths. The allowlist is incomplete and can be bypassed by four equivalent address representations that resolve to the same blocked host but evade the validator's checks:IPv4-mapped IPv6 literals (`[::ffff:169.254.169.254]`, `[::ffff:127.0.0.1]`, `[::ffff:10.0.0.1]`) are returned by `socket.getaddrinfo` as `IPv6Address` objects; `IPv6Address in IPv4Network('169.254.0.0/16')` returns `False`, so the `_check_blocked_networks` and `_check_private_networks` predicates do not match.IPv4 unspecified address `0.0.0.0` is not in `ALWAYS_BLOCKED_NETWORKS` (which covers `127.0.0.0/8` but not `0.0.0.0/8`); on Linux + Docker, `0.0.0.0` routes to local services on any interface, and on dual-stack-mapped sockets it also reaches loopback listeners.A malicious OSCAL profile referencing one of these URLs in `imports[*].href` or `back-matter.resources[*].rlinks[*].href` causes `HTTPSFetcher.__init__` and `_do_fetch` (which both invoke `validator.validate_url`) to pass the URL through to `requests.get`, contacting cloud-metadata services, loopback admin interfaces, or RFC 1918 internal networks (with `TRESTLE_BLOCK_PRIVATE_IPS=true` set) that the validator was specifically designed to block.Affected versions`compliance-trestle` (PyPI) versions `<= 4.0.3` are affected. 4.0.3 (released 2026-05-20) is the latest release and the one that introduced `URLSecurityValidator`; prior releases had no SSRF guard at all.Privilege requiredNetwork-position attacker who can supply or influence an OSCAL artifact (profile / catalog / SSP / component-definition) that compliance-trestle subsequently fetches via `HTTPSFetcher` or `SFTPFetcher`. The most realistic vector is a malicious OSCAL profile whose `imports[*].href` references one of the bypass URLs; the artifact then flows through `trestle href add` / `trestle import` / `trestle assemble` / `trestle author` / any workflow that resolves the profile's imports.Root cause`trestle/core/remote/security.py` (4.0.3, lines 56-71 + 156-167):Four independent gaps:**No IPv4-mapped IPv6 normalization.** `socket.getaddrinfo('::ffff:169.254.169.254', None)` returns an `IPv6Address`. Python's `ipaddress` module raises `TypeError` if mixed types are compared, and the `in` operator suppresses that to `False`. The validator never calls `.ipv4_mapped` to canonicalize before the membership check, so any always-blocked IPv4 range is bypassable via the `[::ffff:N.N.N.N]` literal.**`METADATA_HOSTNAMES` is an exact-string set.** The hostname for `https://[::ffff:169.254.169.254]/` is `::ffff:169.254.169.254`, which is not in the set.**`0.0.0.0` is not blocked.** `0.0.0.0` is not in any of the four `ALWAYS_BLOCKED_NETWORKS` ranges. On Linux and inside containers, connecting to `0.0.0.0` routes to local services on any interface (a common SSRF technique against Docker / orchestrator agents on `0.0.0.0:PORT`).**DNS rebinding ribbon is only one IP deep.** `_resolve_hostname` records the first `getaddrinfo` result set, but a hostname with mixed records can still serve a private IP on the second resolution `validator.validate_url(self._url)` performs in `_do_fetch`. The IPv4-mapped-IPv6 bypass already eliminates the need for rebinding.Sibling code paths sharing the same defect: `SFTPFetcher.__init__` (lines 359-365 of `cache.py`) wires the identical `URLSecurityValidator` and inherits all four gaps.Reproduction (E2E against `pip install compliance-trestle==4.0.3` + local IMDS simulator)Observed output on a supported runtime, Python 3.12.13 / macOS Darwin 25.3.0 (verbatim). Note 3.12.13 is >= 3.12.4, so CPython CVE-2024-4032's `is_global`/`is_private` reclassification IS active here; the bypass nevertheless works because this validator uses `IPv6Address in IPv4Network(...)` membership (which silently returns False for cross-version comparison), NOT the `is_global` predicate. The mechanism is therefore robust to CPython version:(The bracketed-IPv6 diagnostic lines above are the load-bearing proof of `is_global`-independence: even with CPython's CVE-2024-4032 fix active (`is_global=False`, `is_private=True`), the validator's `in IPv4Network(...)` membership check still returns `False`, so the bypass is not contingent on running an older Python. BYPASS-1/BYPASS-4 show the guard passing the URL; their connectivity lines time out only because the local sentinel listens on loopback/`::`, not on those literal addresses -- the security-relevant result is the validator passing, which on a real dual-stack host routes to the embedded IPv4 endpoint.)Negative controls confirm the validator works as designed for the canonical literal forms it was written to block. All four bypass URLs pass `URLSecurityValidator.validate_url()` on the latest patched release.ImpactSSRF to AWS / Azure / GCP / Alibaba IMDS via `https://[::ffff:169.254.169.254]/latest/meta-data/iam/security-credentials/<role>` -> short-lived role credentials exfiltrated through the cached fetch.SSRF to loopback administrative interfaces via `https://0.0.0.0:PORT/` or `https://[::ffff:127.0.0.1]:PORT/` -> access to local-only admin endpoints (Docker socket on `unix://`, Prometheus, etcd, Kubelet) that the validator was supposed to deny.SSRF to RFC 1918 internal services via `https://[::ffff:10.0.0.1]/...` even when `TRESTLE_BLOCK_PRIVATE_IPS=true` is explicitly set, defeating the operator's defense-in-depth posture.The cache-write traversal protection (`PathSecurityValidator.validate_url_path_for_cache` + `validate_cache_path`) is orthogonal and remains effective; this advisory is scoped to the SSRF allowlist gap only.Suggested fixNormalize every resolved IP to its canonical IPv4 form before membership checks, and add `0.0.0.0` to the always-blocked set. Diff sketch against `trestle/core/remote/security.py`:Also add the canonicalized literal to `_check_metadata_endpoints`:This mirrors the canonicalization pattern that pyca/cryptography, rustls-webpki, and the recent Node `undici` SSRF patches converged on after similar IPv6-mapped bypasses surfaced in 2024-2025.CreditReported by tonghuaroot.
CVSS v4.0 score: 8.6