Implementing SSRF Allowlists and Metadata Service Protection: A Secure Coding Guide
Server-Side Request Forgery (SSRF) targeting cloud metadata endpoints remains a critical infrastructure compromise vector. Attackers exploit user-controlled URL parameters to pivot into internal networks, directly querying link-local addresses (e.g., 169.254.169.254, 100.100.100.200, metadata.google.internal) to exfiltrate temporary IAM credentials, instance metadata, and network topology. Traditional perimeter firewalls and regex-based denylists fail against encoding bypasses, DNS rebinding, IPv6 translation, and newly provisioned internal CIDR blocks. The only defensible baseline is strict, parsed URI allowlisting combined with mandatory metadata service hardening.
This guide provides exact implementation steps, secure validation patterns, and infrastructure-as-code enforcement for engineering and compliance teams. For foundational threat modeling, refer to Server-Side Request Forgery (SSRF) Prevention before deploying runtime controls. We will cover schema validation, IMDSv2 enforcement, CI/CD policy-as-code, and runtime egress controls to meet SOC 2, ISO 27001, and PCI-DSS 4.0 requirements.
1. Threat Modeling Metadata Exposure in SSRF Vectors
SSRF bypasses network segmentation by leveraging the application’s own outbound request capabilities. When an application fetches user-supplied URLs, image assets, or webhook endpoints, the underlying HTTP client resolves DNS and routes traffic through the host’s network stack. If the host resides in a cloud environment with default metadata routing, a single unvalidated request to http://169.254.169.254/latest/meta-data/iam/security-credentials/ grants the attacker the instance role’s temporary credentials.
Attack paths typically follow:
- Input Vector: File upload via URL, PDF generation, webhook registration, or image proxy.
- Resolution Phase: DNS lookup translates attacker-controlled domain to
169.254.169.254via DNS rebinding or internal DNS override. - Execution Phase: HTTP client follows redirects, bypasses IPv4/IPv6 translation, or exploits open redirects to reach the metadata endpoint.
- Credential Exfiltration: Temporary STS tokens are captured and used for lateral movement or cloud account takeover.
Network-level denylists are insufficient because they cannot account for dynamic IP allocation, IPv4-mapped IPv6 addresses (::ffff:169.254.169.254), or HTTP/2 multiplexing bypasses. Secure architecture requires explicit trust boundaries at the application layer, enforced before socket connection. Comprehensive mitigation strategies align with established Vulnerability Patterns & Web Mitigation Strategies frameworks, prioritizing allowlist validation and metadata endpoint session enforcement.
2. Architecting Strict URL Allowlists
Strict allowlisting requires parsing the URI, validating the scheme, resolving the hostname, verifying the resolved IP against an explicit allowlist of permitted destinations, and executing the request using the resolved IP to prevent DNS rebinding.
Implementation Requirements
- Protocol Enforcement: Allow
httpsonly. Rejecthttp,file,gopher,ftp, and custom schemes. - Domain Allowlist: Maintain a cryptographically signed or environment-managed list of allowed FQDNs.
- IP Allowlist: Only IPs that resolve to explicitly trusted CIDRs (e.g., your own internal API network) are permitted. All RFC 1918, link-local, loopback, and cloud metadata ranges are blocked.
- DNS Rebinding Mitigation: Resolve DNS before connection. Validate the resolved IP. Pass the resolved IP directly to the HTTP client with the original
Hostheader to prevent re-resolution. - Punycode & Normalization: Convert internationalized domain names (IDN) to ASCII punycode before validation. Strip default ports and path traversal sequences.
Production Python Implementation
import socket
import ipaddress
import urllib.parse
from typing import Set, Tuple
# Explicit allowlist: domains and their expected CIDRs
ALLOWED_DOMAINS: Set[str] = {"api.trusted-partner.com", "cdn.internal-corp.net"}
# Blocked ranges: RFC 1918, loopback, link-local (metadata), and IPv6 private
BLOCKED_NETWORKS = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"), # AWS/GCP/Azure metadata
ipaddress.ip_network("::1/128"),
ipaddress.ip_network("fc00::/7"),
ipaddress.ip_network("fe80::/10"),
]
def _is_ip_blocked(ip_str: str) -> bool:
try:
ip_obj = ipaddress.ip_address(ip_str)
return any(ip_obj in net for net in BLOCKED_NETWORKS)
except ValueError:
return True # Reject unparseable addresses
def validate_and_prepare_request(user_url: str) -> Tuple[str, dict]:
parsed = urllib.parse.urlparse(user_url)
# 1. Protocol enforcement
if parsed.scheme.lower() != "https":
raise ValueError("Only HTTPS is permitted")
# 2. Domain allowlist check
domain = (parsed.hostname or "").lower()
if domain not in ALLOWED_DOMAINS:
raise ValueError(f"Domain '{domain}' not in allowlist")
# 3. DNS resolution — must happen before socket connect (prevents DNS rebinding)
try:
resolved_ips = socket.getaddrinfo(domain, 443, socket.AF_UNSPEC, socket.SOCK_STREAM)
if not resolved_ips:
raise RuntimeError("No DNS results returned")
resolved_ip = resolved_ips[0][4][0]
except socket.gaierror as e:
raise RuntimeError(f"DNS resolution failed: {e}")
# 4. Block any resolved IP in restricted ranges (metadata, RFC 1918, loopback)
if _is_ip_blocked(resolved_ip):
raise ValueError(f"Resolved IP {resolved_ip} is in a blocked range")
# 5. Build request using the pinned IP to prevent re-resolution
safe_url = f"https://{resolved_ip}{parsed.path}"
if parsed.query:
safe_url += f"?{parsed.query}"
headers = {"Host": domain} # Preserve original Host for TLS SNI / virtual hosting
return safe_url, headers
Security Boundary: Never pass the original user-supplied URL directly to requests.get() or httpx. Always use the resolved, validated IP with a sanitized Host header. The function above is correct: it resolves to an IP, then blocks any IP that falls in restricted ranges before constructing the safe request URL.
3. Metadata Service Hardening & IMDSv2 Enforcement
Cloud providers expose metadata endpoints via link-local routing. Hardening requires disabling legacy IMDSv1, enforcing token-based authentication, and restricting network hop counts to prevent container or VM escape.
AWS IMDSv2 Enforcement
Instance Metadata Service v2 (IMDSv2) requires a PUT request to retrieve a session token before any GET request can return credentials. Enforce this at the infrastructure layer to prevent IMDSv1 credential retrieval via a single unauthenticated GET.
Terraform Implementation (AWS, Azure, GCP)
# AWS EC2: Enforce IMDSv2, disable fallback, restrict hops
resource "aws_instance" "secure_app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # Disables IMDSv1
http_put_response_hop_limit = 1 # Prevents container escape (limit to 1 hop)
instance_metadata_tags = "disabled"
}
}
# Azure VM: Use SystemAssigned managed identity (no static credentials needed)
resource "azurerm_linux_virtual_machine" "secure_vm" {
name = "secure-vm"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B2s"
admin_username = "azureuser"
identity {
type = "SystemAssigned"
}
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "20_04-lts"
version = "latest"
}
}
# GCP Compute: Restrict scopes and enable OS Login
resource "google_compute_instance" "secure_instance" {
name = "secure-instance"
machine_type = "e2-medium"
zone = "us-central1-a"
metadata = {
enable-oslogin = "TRUE"
}
service_account {
email = google_service_account.app.email
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
}
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
network = "default"
}
}
Compliance Mapping: CIS AWS Foundations Benchmark v1.5.0 requires http_tokens = "required". PCI-DSS 4.0 Requirement 1.3.1 mandates network segmentation and least-privilege access, enforced here via hop limits and scoped IAM roles.
4. CI/CD Integration & Automated Compliance Checks
Shift-left enforcement prevents metadata exposure before deployment. Integrate static analysis (SAST) to flag unvalidated HTTP clients, and deploy policy-as-code to block insecure infrastructure provisioning.
OPA/Rego Policy for IaC Scanning
This Rego policy blocks Terraform plans that enable IMDSv1 or set hop limits above 1.
package terraform.metadata_security
import rego.v1
deny[msg] if {
resource := input.plan.resource_changes[_]
resource.type == "aws_instance"
resource.change.after.metadata_options.http_tokens != "required"
msg := sprintf("AWS Instance '%s' does not enforce IMDSv2. Set http_tokens = 'required'.", [resource.address])
}
deny[msg] if {
resource := input.plan.resource_changes[_]
resource.type == "aws_instance"
resource.change.after.metadata_options.http_put_response_hop_limit > 1
msg := sprintf("AWS Instance '%s' hop_limit > 1. Restrict to 1 to prevent SSRF pivot.", [resource.address])
}
Pipeline Integration Steps
- Pre-commit: Run
tfsecorcheckovwith custom Rego policies. Fail onCRITICALmetadata misconfigurations. - SAST Scan: Configure Semgrep rules to detect
requests.get(user_input)orfetch(user_input)without preceding validation middleware. - Dynamic Validation: Deploy a canary container during integration tests that attempts
GET http://169.254.169.254/latest/meta-data/. Assert that the response is a connection refusal or HTTP 403, confirming the firewall/IMDSv2 controls are active. - Policy Enforcement Gate: Integrate
opa evalinto the CI pipeline. Reject PRs if anydenyrule triggers.
5. Edge-Case Handling & Runtime Mitigation
Strict allowlists and IaC policies cover baseline scenarios, but advanced bypass techniques require runtime defenses and egress controls.
Advanced Bypass Vectors
- Open Redirect Chaining: Attacker-controlled domains redirect to metadata endpoints after initial validation. Mitigation: Disable automatic redirect following (
allow_redirects=Falsein Python requests,redirect: 'error'in the Fetch API) or re-validate the final destination URL against the allowlist after each3xxresponse. - WebSocket SSRF:
ws://orwss://protocols bypass HTTP allowlists. Mitigation: Explicitly blockws/wssschemes unless explicitly required, and enforce origin validation on WebSocket upgrades. - IPv6 Loopback & Link-Local Bypasses: Attackers supply
::1,::ffff:169.254.169.254, or0x7f000001(hex-encoded loopback). Mitigation: Useipaddress.ip_address()(Python) ornet.ParseIP()(Go) to canonicalize before comparison; both libraries handle IPv4-mapped IPv6 addresses correctly.
Runtime Egress Architecture
Deploy an egress proxy (e.g., Envoy, Squid) with TLS termination and outbound allowlisting. Configure the application to route all external traffic through the proxy. The proxy enforces:
- Strict domain allowlists at the network layer
- Certificate pinning for critical endpoints
- Request rate limiting and anomaly detection
Implement structured audit logging for all outbound requests. Retain logs for 12 months with WORM storage to satisfy SOC 2 CC7.2 and ISO 27001 A.12.4.
Fallback Behavior
When validation fails, return a generic 400 Bad Request or 403 Forbidden. Never expose internal DNS resolution errors, resolved IP addresses, or stack traces. Implement circuit breakers to prevent resource exhaustion from malicious validation loops.
Common Implementation Mistakes
- Relying on regex denylists instead of strict domain/IP allowlists: Denylists cannot enumerate all malicious encodings, IPv6 variants, or newly allocated cloud IPs.
- Validating the hostname but not the resolved IP: DNS rebinding attacks swap the resolved IP post-validation. Always resolve DNS and validate the resolved IP immediately before socket connection.
- Ignoring IPv6 link-local and IPv4-mapped IPv6 address bypasses:
::ffff:169.254.169.254andfe80::/10bypass IPv4-only filters. Use canonical IP address parsing before comparison. - Deploying IMDSv2 without setting
http_tokens = "required": The default"optional"allows legacy IMDSv1 access. Always set"required"to enforce the session token flow. - Hardcoding allowlists without environment-aware configuration management: Static allowlists break across staging/production. Use environment variables, secret managers, or dynamic configuration services with cryptographic signing.
FAQ
Why are denylists insufficient for SSRF mitigation against metadata services?
Denylists fail due to encoding bypasses (%2e%2e/), DNS rebinding, IPv6/IPv4 translation (::ffff:169.254.169.254), and dynamic cloud IP allocation. Attackers can easily rotate domains or use newly provisioned internal CIDRs. Allowlists enforce explicit trust boundaries, reducing the attack surface to known, audited endpoints.
How do I prevent DNS rebinding when validating SSRF allowlists?
Resolve the domain to an IP address, verify the IP is not in any blocked range, and then initiate the HTTP request using the resolved IP directly while preserving the original Host header. This pins the connection to the validated IP rather than letting the OS re-resolve during the TCP handshake.
What compliance frameworks mandate metadata service protection? SOC 2 CC6.1 and CC7.2 require network segmentation and monitoring of credential access. ISO 27001 A.13.1.1 mandates network controls to prevent unauthorized access. PCI-DSS 4.0 Requirements 1.3.1 and 2.2.7 explicitly require least-privilege IAM and metadata endpoint hardening to prevent credential exfiltration. CIS Benchmarks provide technical baselines for IMDSv2 enforcement across AWS, Azure, and GCP.