An agent that can fetch(url) will eventually fetch http://169.254.169.254/. SSRF defenses are not optional: allowlists, DNS pinning, and egress proxies that block link-local and cloud metadata.
⚡ TL;DR: Default-deny URL tools. Allow only https to known hosts. Resolve DNS yourself and block private/link-local ranges. Force egress through a proxy that strips IMDS.
Allowlist first
# net/allow.py
from urllib.parse import urlparse
import ipaddress, socket
ALLOW_HOSTS = {"api.github.com", "docs.internal.example"}
def assert_safe_url(url: str):
u = urlparse(url)
if u.scheme != "https":
raise ValueError("scheme")
if u.hostname not in ALLOW_HOSTS:
raise ValueError("host")
infos = socket.getaddrinfo(u.hostname, 443)
for info in infos:
ip = ipaddress.ip_address(info[4][0])
if ip.is_private or ip.is_loopback or ip.is_link_local:
raise ValueError("blocked_ip") # ✅ DNS rebinding defense
Egress proxy
# block IMDS and RFC1918
deny 169.254.0.0/16;
deny 10.0.0.0/8;
deny 172.16.0.0/12;
deny 192.168.0.0/16;
Run agent tasks with careful NO_PROXY — never let the task hit IMDS hop-by-hop.
Tool shape
export async function httpGet(url: string) {
assertSafeUrl(url);
return fetch(url, { redirect: "error" }); // ❌ no blind redirects
}
Closing checklist
- [ ] HTTPS + host allowlists
- [ ] Resolve + block private IPs
- [ ] Disable automatic redirects or re-validate
- [ ] Egress proxy denying IMDS
- [ ] Unit tests for metadata IPs and rebinding
Series navigation
Day 61: Prompt Injection in Jira, Email, and RAG · Day 63: IAM for Agents: Roles, Not God Keys
Last updated September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
