Repository navigation
allow_private=True also switches off the IANA special-use blocklist #1357
Description
adcp.signing.validate_resolved_ip puts its entire blocked set behind one
boolean. allow_private=True is documented as "skip the reserved-range check.
For tests only" — a caller enables it to dial a test's own loopback origin or a
container-bridge address. It also admits every IANA special-use range the
validator knows, including the ranges PR #974 added for exactly the opposite
reason.
What the flag relaxes
Measured against adcp 8.0.0 on CPython 3.14.6, one address per range:
| Address | Range | allow_private=False |
allow_private=True |
|---|---|---|---|
10.0.0.1 |
RFC 1918 | REFUSE | ACCEPT |
172.16.0.1 |
RFC 1918 | REFUSE | ACCEPT |
192.168.1.1 |
RFC 1918 | REFUSE | ACCEPT |
127.0.0.1 |
RFC 1122 loopback | REFUSE | ACCEPT |
::1 |
RFC 4291 loopback | REFUSE | ACCEPT |
fd00::1 |
RFC 4193 unique local | REFUSE | ACCEPT |
100.64.0.1 |
RFC 6598 carrier-grade NAT | REFUSE | ACCEPT |
192.88.99.1 |
RFC 7526 6to4 relay anycast | REFUSE | ACCEPT |
192.31.196.1 |
RFC 7535 AS112-v4 anycast | REFUSE | ACCEPT |
192.52.193.1 |
RFC 7450 AMT anycast | REFUSE | ACCEPT |
192.175.48.1 |
RFC 7534 AS112 direct delegation | REFUSE | ACCEPT |
2001:20::1 |
RFC 7343 ORCHIDv2 | REFUSE | ACCEPT |
169.254.1.1 |
RFC 3927 link-local | REFUSE | ACCEPT |
fe80::1 |
RFC 4291 link-local | REFUSE | ACCEPT |
224.0.0.1 |
RFC 5771 multicast | REFUSE | ACCEPT |
ff02::1 |
RFC 4291 multicast | REFUSE | ACCEPT |
0.0.0.0 |
RFC 1122 unspecified | REFUSE | ACCEPT |
:: |
RFC 4291 unspecified | REFUSE | ACCEPT |
192.0.2.1 |
RFC 5737 TEST-NET-1 | REFUSE | ACCEPT |
198.51.100.1 |
RFC 5737 TEST-NET-2 | REFUSE | ACCEPT |
203.0.113.1 |
RFC 5737 TEST-NET-3 | REFUSE | ACCEPT |
2001:db8::1 |
RFC 3849 documentation | REFUSE | ACCEPT |
198.18.0.1 |
RFC 2544 benchmarking | REFUSE | ACCEPT |
192.0.0.1 |
RFC 6890 protocol assignments | REFUSE | ACCEPT |
240.0.0.1 |
RFC 1112 240.0.0.0/4 |
REFUSE | ACCEPT |
2002::1 |
RFC 3056 6to4 | REFUSE | ACCEPT |
2001::1 |
RFC 4380 Teredo | REFUSE | ACCEPT |
64:ff9b::1 |
RFC 6052 NAT64 | REFUSE | ACCEPT |
100::1 |
RFC 6666 discard-only | REFUSE | ACCEPT |
169.254.169.254 |
AWS/Azure/GCP metadata | REFUSE | REFUSE |
100.100.100.200 |
Alibaba metadata | REFUSE | REFUSE |
192.0.0.192 |
Oracle metadata | REFUSE | REFUSE |
fd00:ec2::254 |
AWS IPv6 metadata | REFUSE | REFUSE |
The first six rows are what the flag's name and docstring describe. The 23 bold
rows are not private address space. 100.64.0.0/10 is the range AdCP 3.1.1
names in the deny list a fetcher MUST apply ("Webhook URL validation (SSRF)",
step 2), and the module's own comment records why it is there: "routable inside
carrier and container networks (and is where Alibaba's metadata endpoint
lives), so leaving it open is a real SSRF path into an operator's internal
network." allow_private=True leaves it open.
The bold rows also show why a flag-level relaxation cannot be narrow.
ipaddress.is_private is a far wider predicate than RFC 1918: it reports True
for RFC 5737 and RFC 3849 documentation space, RFC 2544 benchmarking, RFC 6890
protocol assignments, 6to4, Teredo, RFC 6666 discard-only and 240.0.0.0/4.
None of that is private address space, and relaxing the flag relaxes all of it.
Runnable
import ipaddress
from adcp.signing import validate_resolved_ip
from adcp.signing.jwks import SSRFValidationError
for addr in ("100.64.0.1", "192.88.99.1", "2001:20::1", "169.254.1.1", "224.0.0.1"):
ip = ipaddress.ip_address(addr)
for allow_private in (False, True):
try:
validate_resolved_ip(ip, allow_private=allow_private)
verdict = "ACCEPT"
except SSRFValidationError:
verdict = "REFUSE"
print(f"{addr:16} allow_private={allow_private!s:5} -> {verdict}")Output on adcp 8.0.0:
100.64.0.1 allow_private=False -> REFUSE
100.64.0.1 allow_private=True -> ACCEPT
192.88.99.1 allow_private=False -> REFUSE
192.88.99.1 allow_private=True -> ACCEPT
2001:20::1 allow_private=False -> REFUSE
2001:20::1 allow_private=True -> ACCEPT
169.254.1.1 allow_private=False -> REFUSE
169.254.1.1 allow_private=True -> ACCEPT
224.0.0.1 allow_private=False -> REFUSE
224.0.0.1 allow_private=True -> ACCEPT
Layer: API DESIGN
One hand-authored signature choice. The ranges are correct, the predicate is
correct, the list PR #974 added is complete. The flag's scope is drawn across
two concerns instead of one.
Downstream consequence
A caller has no way to ask for the thing the flag is named for. The two range
tables (_EXTRA_BLOCKED_NETWORKS, BLOCKED_METADATA_IPS) are module-private
and absent from adcp.signing.__all__, and the only knob is the boolean:
>>> import adcp.signing as S
>>> [n for n in S.__all__ if "BLOCK" in n.upper() or "NETWORK" in n.upper()]
[]
>>> hasattr(S, "_EXTRA_BLOCKED_NETWORKS")
FalseAn adopter that wants loopback for its test origins and the special-use
blocklist for its buyer-supplied webhook URLs re-enumerates the ranges in its
own code and applies them outside the SDK call. Where the dialled URL is
publisher- or buyer-supplied — jwks_uri, authoritative_location,
pushNotificationConfig.url — that is the posture the SDK should be providing.
Fix
Split the blocked set at the reachability line and give each half its own gate:
allow_private for RFC 1918, RFC 4193 and loopback; allow_special_use for
the rest. Cloud metadata IPs stay blocked under both. PR to follow.
Related
One instance of a pattern in src/adcp/signing/: a rule declared in one place
and decided in another. #1354 enumerates the others.
Activity
bokelley commentedon Oct 3, 2026
Triage
Classification: Bug — API design / security
Bucket(s): signing (label not in repo — gap noted below)
Status: ready-for-human
Milestone: —
What the experts said:
100.64.0.0/10(CGNAT); migration guide endorsesallow_private=Trueas a dev/CI escape hatch without noting CGNAT side-effects; high severity; breaking change with a clear migration path (allow_special_use=True).100.64.0.0/10admits no test-mode carve-out; the two-parameter split (allow_private/allow_special_use) is spec-conformant.My take: Both experts confirm a real, high-severity SSRF path and agree the proposed fix is correct. Flagging for
@bokelleybecause the issue is security-sensitive and the fix is a breaking behavior change to a public parameter.Implementation scope (for the PR author):
allow_special_use: bool = Falsetovalidate_resolved_ip,resolve_and_validate_host,validate_jwks_uri,build_ip_pinned_transport,build_async_ip_pinned_transport,CachingJwksResolver,AsyncCachingJwksResolver_EXTRA_BLOCKED_NETWORKScheck to theallow_special_usegate;allow_privatecovers onlyis_private,is_loopback,is_link_local,is_multicast,is_reserved,is_unspecifiedallow_private=Truemust audit their environment before upgradingallow_private=Truealone blocks CGNAT post-split;allow_special_use=Truerequired to pass itLabel gap:
signingandsecuritylabels do not exist in this repo. Appliedbugonly.Triaged by Claude Code. Session: https://claude.ai/code/session_01DK1F8fKDmCSxz5m8LBELq5
Generated by Claude Code
fix(signing)!: stop `allow_private` from admitting IANA special-use r…