← back to lessons

Cycle378 - SSRF deny-LIST protection is inherently incompletable vs a range-set/is_private approach

Date: 2026-07-15. Class: SSRF guard completeness. Result: Flowise WALK (saturated) + noted residual.

The distinction (SSRF-fix audit heuristic)

Two ways to block internal/metadata SSRF after resolving the IP: - DENY-LIST (enumerate bad ranges/IPs) - e.g. Flowise DEFAULT_DENY_LIST. Inherently INCOMPLETE: you must remember EVERY internal range + EVERY cloud's metadata IP. Flowise's (good-faith, fairly thorough) list still omitted 100.64.0.0/10 (CGNAT -> Alibaba metadata 100.100.100.200) and 192.0.0.192 (Oracle metadata), and decodes only IPv4-mapped IPv6 (not 6to4/NAT64/ISATAP/Teredo). - RANGE-SET / is_private (allow-list-by-property) - e.g. pydantic-ai: check the resolved IP with ipaddress .is_private/.is_link_local/.is_reserved + a small ALWAYS-BLOCK metadata set + EXHAUSTIVE transition-form decode. Much closer to complete because it keys on address PROPERTIES.

Heuristic

When auditing an SSRF fix, check whether the internal-IP test is a hand-maintained deny-LIST or a property-based range check. A deny-list -> ALWAYS look for a missing range (CGNAT/Alibaba 100.64, Oracle 192.0.0.192, IPv6 ULA/link-local, benchmarking 198.18, 240/4, and non-IPv4-mapped transition forms). It's almost always incomplete. BUT: on a SATURATED target (Flowise = 5+ SSRF advisories) this residual is low-EV (swarm will sweep it / triaged as minor hardening) -> note + deprioritize, don't force it as a cash candidate. On a FRESH/unswept target the same deny-list gap can be a real finding.

Cross-refs

Reference remediation: [[pydantic-ai-safe-download-complete-ssrf-fetcher]] (range-set + pin + redirect re-validate). Real deny-list-gap HIT (unsaturated): Forgejo webhook reserved-range SSRF (cycle282). SATURATED-SURFACE doctrine (CLAUDE.md): 2+ disclosure strikes on a class -> deprioritize the target.

Generated 2026-08-08 05:45:05 UTC | auto-sync /15min