Skip to content

SafeHttpClient Egress & Anti-SSRF Model

Egress & SSRF Threat Model (SafeHttpClient)

Section titled “Egress & SSRF Threat Model (SafeHttpClient)”

Every outbound HTTP request made by resolver-core passes through safe_client.rs.

ANTI-SSRF KERNEL GUARD
DID.is Architecture Invariant #2

The resolver treats all non-public network space as hostile. If DNS resolution for any endpoint (DID document, DIF domain configuration, or status list) yields an IP address within RFC 1918, RFC 6890, or cloud metadata CIDRs, the request is aborted prior to socket creation.


Live RFC 9457 Egress Interception Telemetry

Section titled “Live RFC 9457 Egress Interception Telemetry”

When an untrusted input triggers an SSRF block, the resolver returns an instant RFC 9457 Problem Details payload with microsecond trace telemetry:

403 ForbiddenEGRESS_BLOCKED
RFC 9457 PROBLEM DETAILS

Outbound Egress Blocked by Security Policy

Request destination '10.0.4.15:80' resolves to RFC 1918 private IPv4 space. Outbound connection aborted by SafeHttpClient kernel filter.

TRACE INSTANCE:urn:didis:trace:9f8e7d6c-5b4a-3210-fedc-ba9876543210
1
Input Parameter NormalizationStrict URI syntax check completed
PASSED0.14ms
2
DNS Resolution (DoH)A record resolved: 10.0.4.15
PASSED1.42ms
3
SafeHttpClient Egress RuleCIDR Match: 10.0.0.0/8 (RFC 1918 private block)
BLOCKED0.08ms
4
TCP Socket ConnectionTerminated before socket allocation
SKIPPED0.00ms
5
RFC 9457 Error GenerationStandardized JSON problem rendered
PASSED0.06ms
Content-Type: application/problem+json
{
  "type": "https://did.is/errors/egress-blocked",
  "title": "Outbound Egress Blocked by Security Policy",
  "status": 403,
  "code": "EGRESS_BLOCKED",
  "detail": "Request destination '10.0.4.15:80' resolves to RFC 1918 private IPv4 space. Outbound connection aborted by SafeHttpClient kernel filter.",
  "instance": "urn:didis:trace:9f8e7d6c-5b4a-3210-fedc-ba9876543210",
  "timestamp": "2026-10-10T20:58:42.821Z"
}

Prior to connection establishment, every IP address returned by DNS is evaluated against static CIDR denylists. If any IP falls within a non-public subnet, the request fails with ResolverError::SsrfDetected.

DNS Query Hostname ──► [ IP_1, IP_2, ... ]
│
▼ Validate ALL IP addresses
Is ANY IP in blocked CIDR ranges?
├── YES ──► ABORT: SsrfDetected
└── NO ──► Pin socket to IP_1
  • 0.0.0.0/8 (Local identification)
  • 10.0.0.0/8 (RFC 1918 Private)
  • 100.64.0.0/10 (RFC 6598 Carrier-Grade NAT)
  • 127.0.0.0/8 (Loopback)
  • 169.254.0.0/16 (Link-Local / Cloud Metadata 169.254.169.254)
  • 172.16.0.0/12 (RFC 1918 Private)
  • 192.0.0.0/24 (IETF Protocol Assignments)
  • 192.0.2.0/24 (TEST-NET-1)
  • 192.88.99.0/24 (6to4 Relay Anycast)
  • 192.168.0.0/16 (RFC 1918 Private)
  • 198.18.0.0/15 (Network Benchmark Tests)
  • 198.51.100.0/24 (TEST-NET-2)
  • 203.0.113.0/24 (TEST-NET-3)
  • 224.0.0.0/4 (Multicast)
  • 240.0.0.0/4 (Reserved for future use)
  • 255.255.255.255/32 (Limited Broadcast)
  • ::1/128 (Loopback)
  • ::/128 (Unspecified)
  • ::ffff:0:0/96 (IPv4-mapped addresses; mapped IPv4 evaluated against IPv4 denylist)
  • 64:ff9b::/96 (IPv4-IPv6 translation)
  • 64:ff9b:1::/48 (Local IPv4-IPv6 translation)
  • 100::/64 (Discard-only)
  • 2001::/32 (TEREDO)
  • 2001:db8::/32 (Documentation)
  • 2002::/16 (6to4)
  • fc00::/7 (Unique Local Unicast ULA)
  • fe80::/10 (Link-Local Unicast)
  • fec0::/10 (Site-Local Unicast)
  • ff00::/8 (Multicast)

To defeat Time-of-Check to Time-of-Use (TOCTOU) DNS-rebinding attacks:

  1. SafeHttpClient performs asynchronous DNS resolution via tokio::net::lookup_host.
  2. Verifies that all returned SocketAddr entries pass validate_ip.
  3. Selects the first vetted address (addrs[0]) and pins the subsequent HTTP client socket connection directly to that specific socket address.
  4. The remote server cannot rebind the DNS record to an internal IP between resolution and connection.

  1. Manual Hop Re-Validation: Automatic redirect following is disabled. Redirects are handled manually up to MAX_REDIRECTS = 2.
  2. Hop Validation: Each redirect target URL undergoes full URL parsing, scheme checking, host extraction, DNS pre-resolution, and IP denylist validation.
  3. Downgrade Prohibition: Redirects from https:// to http:// are rejected with ResolverError::SsrfDetected.
  4. Concurrency Bounds: Egress concurrency is bounded via a global semaphore (MAX_CONCURRENT_EGRESS = 64) to prevent worker starvation under outbound loads.

Per Architecture Decision Record 002 (ADR-002), DID.is never dereferences @context URLs at runtime.

All recognized vocabularies are compiled statically into context_catalog.rs:

context_catalog.rs
Run

                1
                pub const KNOWN_CONTEXTS: &[KnownContext] = &[
              
                2
                  KnownContext { uri: "https://www.w3.org/ns/did/v1", name: "DID Core v1.0" },
              
                3
                  KnownContext { uri: "https://www.w3.org/ns/did/v1.1", name: "DID Core v1.1" },
              
                4
                  KnownContext { uri: "https://w3id.org/did/v1", name: "DID Core (legacy alias)" },
              
                5
                  KnownContext { uri: "https://www.w3.org/ns/cid/v1", name: "Controlled Identifiers v1.0" },
              
                6
                  KnownContext { uri: "https://w3id.org/security/multikey/v1", name: "Multikey" },
              
                7
                  KnownContext { uri: "https://w3id.org/security/jwk/v1", name: "JsonWebKey" },
              
                8
                  KnownContext { uri: "https://w3id.org/security/data-integrity/v2", name: "Data Integrity v2" },
              
                9
                  KnownContext { uri: "https://w3id.org/security/data-integrity/v1", name: "Data Integrity v1" },
              
                10
                  KnownContext { uri: "https://w3id.org/security/suites/ed25519-2020/v1", name: "Ed25519 2020 suite" },
              
                11
                  KnownContext { uri: "https://w3id.org/security/suites/ed25519-2018/v1", name: "Ed25519 2018 suite" },
              
                12
                  KnownContext { uri: "https://w3id.org/security/suites/x25519-2020/v1", name: "X25519 2020 suite" },
              
                13
                  KnownContext { uri: "https://w3id.org/security/suites/x25519-2019/v1", name: "X25519 2019 suite" },
              
                14
                  KnownContext { uri: "https://w3id.org/security/suites/jws-2020/v1", name: "JWS 2020 suite" },
              
                15
                  KnownContext { uri: "https://w3id.org/security/suites/secp256k1-2019/v1", name: "secp256k1 2019 suite" },
              
                16
                  KnownContext { uri: "https://w3id.org/security/v1", name: "Security vocabulary v1" },
              
                17
                  KnownContext { uri: "https://w3id.org/security/v2", name: "Security vocabulary v2" },
              
                18
                  KnownContext { uri: "https://www.w3.org/ns/credentials/v2", name: "VC Data Model v2.0" },
              
                19
                  KnownContext { uri: "https://www.w3.org/2018/credentials/v1", name: "VC Data Model v1.1" },
              
                20
                  KnownContext { uri: "https://www.w3.org/ns/credentials/examples/v2", name: "VC examples" },
              
                21
                  KnownContext { uri: "https://identity.foundation/.well-known/did-configuration/v1", name: "DIF Well Known DID Configuration" },
              
                22
                  KnownContext { uri: "https://identity.foundation/linked-vp/contexts/v1", name: "DIF Linked VP" },
              
                23
                  KnownContext { uri: "https://identity.foundation/did-webvh/v1", name: "did:webvh v1" },
              
                24
                ];
              

Unknown contexts are flagged in evidence output but never trigger network requests.