compliance-trestle has an URLSecurityValidator SSRF allowlist bypass via IPv4-mapped IPv6 and 0.0.0.0 (CVE-2026-52776)
### 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 required Network-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): ```python ALWAYS_BLOCKED_NETWORKS = [ ipaddress.ip_network('127.0.0.0/8'), # IPv4 loopback only ipaddress.ip_network('::1/128'), # IPv6 loopback (single address) ipaddress.ip_network('169.254.0.0/16'), # IPv4 link-local only ipaddress.ip_network('fe80::/10'), # IPv6 link-local ] METADATA_HOSTNAMES = { '169.254.169.254', # IPv4 literal only 'metadata.google.internal', 'metadata.azure.com', '100.100.100.200', } def _check_blocked_networks(self, ip_addr, hostname): for network in ALWAYS_BLOCKED_NETWORKS: if ip_addr in network: # IPv6Address in IPv4Network -> False raise TrestleError(...) ``` Four independent gaps: 1. **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. 2. **`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. 3. **`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`). 4. **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) ```bash # 1. Setup mkdir -p /tmp/poc-trestle && cd /tmp/poc-trestle python3.12 -m venv venv # any supported runtime (requires-python >= 3.10); 3.12.13 chosen because >= 3.12.4 it carries CPython CVE-2024-4032's is_global fix, proving this bypass is is_global-INDEPENDENT ./venv/bin/pip install --quiet compliance-trestle==4.0.3 ./venv/bin/pip show compliance-trestle | head -2 # Name: compliance-trestle # Version: 4.0.3 # 2. Driver cat > e2e_full.py <<'PY' import http.server, http.client, socket, socketserver, threading, time, os from urllib.parse import urlparse from trestl
compliance-trestle has an URLSecurityValidator SSRF allowlist bypass via IPv4-mapped IPv6 and 0.0.0.0 (CVE-2026-52776)
Description
### 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 required Network-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): ```python ALWAYS_BLOCKED_NETWORKS = [ ipaddress.ip_network('127.0.0.0/8'), # IPv4 loopback only ipaddress.ip_network('::1/128'), # IPv6 loopback (single address) ipaddress.ip_network('169.254.0.0/16'), # IPv4 link-local only ipaddress.ip_network('fe80::/10'), # IPv6 link-local ] METADATA_HOSTNAMES = { '169.254.169.254', # IPv4 literal only 'metadata.google.internal', 'metadata.azure.com', '100.100.100.200', } def _check_blocked_networks(self, ip_addr, hostname): for network in ALWAYS_BLOCKED_NETWORKS: if ip_addr in network: # IPv6Address in IPv4Network -> False raise TrestleError(...) ``` Four independent gaps: 1. **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. 2. **`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. 3. **`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`). 4. **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) ```bash # 1. Setup mkdir -p /tmp/poc-trestle && cd /tmp/poc-trestle python3.12 -m venv venv # any supported runtime (requires-python >= 3.10); 3.12.13 chosen because >= 3.12.4 it carries CPython CVE-2024-4032's is_global fix, proving this bypass is is_global-INDEPENDENT ./venv/bin/pip install --quiet compliance-trestle==4.0.3 ./venv/bin/pip show compliance-trestle | head -2 # Name: compliance-trestle # Version: 4.0.3 # 2. Driver cat > e2e_full.py <<'PY' import http.server, http.client, socket, socketserver, threading, time, os from urllib.parse import urlparse from trestl
CVSS v4.0
Affected software
Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.
Technical Details
- Gcve Source
- db.gcve.eu
- Osv Id
- GHSA-h47f-gmjp-m7rr
- Osv Schema Version
- 1.4.0
- Aliases
- ["CVE-2026-52776"]
- Ecosystems
- ["PyPI"]
- Database Specific Severity
- HIGH
- Cvss Version
- 4.0
Threat ID: 6a7c9b2bbf8831d539cdb508
Added to database: 08/12/2026, 16:11:23 UTC
Last updated: 08/12/2026, 16:11:23 UTC
Views: 1
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.