Preventing SSRF Attacks in AI Agent Workflows
AI agent builders that allow HTTP requests are vulnerable to SSRF attacks. Here's how TopFlow prevents them with URL validation, private IP blocking, and allowlist enforcement.
What is SSRF?
Server-Side Request Forgery (SSRF) is a vulnerability that lets an attacker make the server issue HTTP requests to destinations they choose — not the application developer. In AI agent workflows that expose an HTTP-request node, every user-configured URL is a potential SSRF vector.
Real-World Impact
- Steal AWS/GCP credentials from the cloud metadata endpoint (169.254.169.254)
- Enumerate internal services not exposed to the internet
- Access databases via private IP ranges (10.x, 172.16.x, 192.168.x)
- Read SSH keys and service tokens from cloud provider metadata
TopFlow's SSRF Prevention Strategy
TopFlow's HTTP-request node lets users make arbitrary outbound calls. The guard lives in lib/security/ssrf.ts (view on GitHub) and is applied by the execution engine before any network call is made.
1. Scheme Allowlist
Only http:// and https:// are accepted. file://, ftp://, gopher://, and any other scheme are rejected immediately.
2. Loopback Blocking
localhost, 127.0.0.0/8, ::1, ip6-localhost, and ip6-loopback are all blocked.
3. Private IP Blocking
RFC 1918 ranges (10.x, 172.16–31.x, 192.168.x), CGNAT (100.64–127.x), and multicast/reserved ranges are blocked.
4. Cloud Metadata Blocking
169.254.169.254 (AWS/Azure metadata), metadata.google.internal, and any *.internal / *.local hostname are blocked.
5. Provenance-Aware Exemption
Engine-generated routes (e.g. the scanner's /api/scan/github) bypass the SSRF check because they are set by the engine, not user input. User-supplied URLs are never trusted.
The Code
The guard is a pure function with no side effects — easy to test and trivial to call from the execution engine. There are two entry points: a throwing variant for the hot path, and a non-throwing variant for validation UI.
// lib/security/ssrf.ts
export class SsrfBlockedError extends Error {
constructor(message: string) {
super(message)
this.name = "SsrfBlockedError"
}
}
/** Non-throwing variant — used by validation UI */
export function checkOutboundUrl(rawUrl: string): { safe: boolean; reason?: string } {
let u: URL
try { u = new URL(rawUrl) } catch {
return { safe: false, reason: `invalid URL "${rawUrl}"` }
}
if (u.protocol !== "http:" && u.protocol !== "https:") {
return { safe: false, reason: `blocked scheme "${u.protocol}"` }
}
if (isBlockedHost(u.hostname)) {
return { safe: false, reason: `blocked host "${u.hostname}"` }
}
return { safe: true }
}
/** Throwing variant — used by the execution engine */
export function assertSafeOutboundUrl(rawUrl: string): void {
const result = checkOutboundUrl(rawUrl)
if (!result.safe) throw new SsrfBlockedError(`SSRF blocked: ${result.reason}`)
}
// Private IP ranges covered by isBlockedIpv4():
// 0.0.0.0/8 loopback "this host"
// 10.0.0.0/8 RFC 1918 private
// 127.0.0.0/8 loopback
// 169.254.0.0/16 link-local (AWS/Azure metadata lives here)
// 172.16.0.0/12 RFC 1918 private
// 192.168.0.0/16 RFC 1918 private
// 100.64.0.0/10 CGNAT
// 224.0.0.0/4+ multicast / reservedThe Provenance Distinction
TopFlow's GitHub Security Scanner calls an internal route — /api/scan/github — from within the workflow engine. If the SSRF guard checked this URL, it would block the feature (it's a relative path, not a public hostname).
The fix is provenance: the engine knows it generated this URL, so it never passes it through assertSafeOutboundUrl. Only URLs that originated from user-configured node data are checked. This is documented in the source file comment and is the correct architectural answer — never add a special-case bypass inside the guard itself.
Honest Limitation: DNS Rebinding
This guard validates hostnames and IP literals only — it does not resolve DNS. An attacker who controls a public domain can configure it to resolve to a private IP (DNS rebinding). The first request clears the check; a second request (after a TTL flip) hits the internal target. Mitigation requires DNS resolution at validation time or a network-level egress filter. This is a documented residual risk; see Tutorial 01 for the full attack tree.
Key Takeaways
- Use a blocklist for known-bad ranges (private IPs, cloud metadata), not an allowlist for every legitimate API
- Separate user-controlled URLs from engine-generated routes — don't bypass the guard, use provenance
- Provide both a throwing and non-throwing variant so validation UI and the hot path share one implementation
- Document DNS-rebinding as a residual risk — honesty in security docs builds more trust than false completeness
- Layer the guard with rate limiting and cycle detection; SSRF protection is one layer, not a complete defense
Go Deeper
Tutorial 01 covers this guard in full — threat model, attack trees (including the ones not fully closed), design trade-offs, and a hands-on lab where you attempt an SSRF against a local TopFlow instance.