SSRF Protection¶
Server-Side Request Forgery (SSRF) protection prevents AI agents from accessing private networks, cloud metadata endpoints, and dangerous URL schemes.
⚠️ Important Limitations¶
ai-guardian's SSRF protection is pattern-based filtering, not comprehensive network security.
What It CAN Protect Against¶
✅ Bash commands with explicit URLs:
✅ Tool parameters containing private IPs:
WebFetch(url="http://169.254.169.254") # ❌ BLOCKED
mcp__custom__fetch(url="http://internal") # ❌ BLOCKED
What It CANNOT Protect Against¶
❌ MCP server internal calls:
# ai-guardian sees: source="web"
# But MCP server internally calls: http://169.254.169.254
mcp__notebooklm__research_start(source="web") # ✅ ALLOWED (can't see internal call)
❌ Other undetectable scenarios: - Dynamic URL construction inside tools - HTTP redirects after tool execution starts - IDE's own network requests - Binary protocol inspection
Why These Limitations Exist¶
ai-guardian is hook-based, not a network proxy: - Hooks fire before tool execution (PreToolUse) - We see command strings and tool parameters - We do NOT see runtime network traffic - MCP servers execute after the hook approves them
Architecture: ai-guardian cannot intercept network calls - it can only inspect text strings.
For Comprehensive SSRF Protection¶
ai-guardian provides pattern-based filtering only. For complete protection:
1. Network-level controls (REQUIRED):
# Firewall rules blocking metadata endpoints
sudo iptables -A OUTPUT -d 169.254.169.254 -j REJECT
sudo iptables -A OUTPUT -d 10.0.0.0/8 -j REJECT
# Cloud provider network policies
# AWS: VPC egress rules
# GCP: Firewall rules
# Azure: Network Security Groups
2. MCP server sandboxing (RECOMMENDED): - Run MCP servers in Docker containers with network policies - Use VMs with restricted network access - Only install MCP servers from trusted sources
3. Supply chain verification (PLANNED): - Verify MCP server signatures - Code review before installation - Allowlist trusted publishers only
Bottom Line¶
ai-guardian catches obvious SSRF attempts in command strings but cannot replace network-level security. Think of it as a "basic syntax check" that prevents copy-paste mistakes, not comprehensive network protection.
Overview¶
SSRF attacks allow attackers to make the AI agent send requests to unintended locations: - Credential theft: Access cloud metadata endpoints to steal AWS/GCP/Azure credentials - Internal network scanning: Probe private network services - Local file access: Read local files via file:// URLs - Firewall bypass: Access internal services from outside the network
AI Guardian's SSRF protection blocks these attacks by checking all Bash commands and tool parameters for dangerous URLs before execution.
Note: This is pattern-based filtering. See Important Limitations above for what it can and cannot protect against.
Core Protections (Immutable - Pattern Matching)¶
These protections CANNOT be disabled via configuration:
Private IP Ranges (RFC 1918 + Loopback + Link-local)¶
IPv4:
- 10.0.0.0/8 - Private network (Class A)
- 172.16.0.0/12 - Private network (Class B)
- 192.168.0.0/16 - Private network (Class C)
- 127.0.0.0/8 - Loopback (localhost)
- 169.254.0.0/16 - Link-local (AWS/Azure metadata)
IPv6:
- ::1/128 - Loopback
- fc00::/7 - Unique local addresses (private network)
- fe80::/10 - Link-local addresses
Cloud Metadata Endpoints¶
AWS:
- 169.254.169.254 - IPv4 metadata endpoint
- fd00:ec2::254 - IPv6 metadata endpoint
- instance-data - Instance metadata service
Google Cloud Platform:
- metadata.google.internal - Primary metadata endpoint
- metadata.goog - Alternative metadata endpoint
Azure:
- 169.254.169.254 - Shared with AWS (same IP range)
Dangerous URL Schemes¶
file://- Local filesystem accessgopher://- Legacy protocol (attack vector)ftp://- File transfer protocolftps://- Secure FTPdata://- Data URLs (can encode arbitrary content)dict://- DICT protocolldap://- LDAP protocolldaps://- Secure LDAP
Configuration¶
Basic Configuration¶
Default configuration (~/.config/ai-guardian/ai-guardian.json):
Full Configuration¶
{
"ssrf_protection": {
"enabled": true,
"action": "block",
"additional_blocked_ips": [
"203.0.113.0/24",
"198.51.100.0/24"
],
"additional_blocked_domains": [
"internal.example.com",
"admin.local",
"*.corp.internal"
],
"allowed_domains": [
"api.corp.internal",
"public.staging.example.com"
],
"allow_localhost": false
}
}
Configuration Options¶
enabled (boolean, default: true)¶
Enable or disable SSRF protection entirely.
Example:
Time-based enabling (NEW in v1.5.0):
action (string, default: "block")¶
Action to take when SSRF is detected:
- "block" - Prevent execution and show error message (recommended)
- "warn" - Log violation, show warning to user, but allow execution
- "log-only" - Log violation silently without user warning, allow execution
Block mode (default, recommended):
Warn mode (for testing/debugging):
Log-only mode (for monitoring without blocking):
additional_blocked_ips (array, default: [])¶
Additional IP addresses or CIDR ranges to block beyond core protections.
Supports:
- Single IPv4 addresses: "203.0.113.5"
- IPv4 CIDR ranges: "203.0.113.0/24"
- Single IPv6 addresses: "2001:db8::1"
- IPv6 CIDR ranges: "2001:db8::/32"
Example:
{
"ssrf_protection": {
"additional_blocked_ips": [
"203.0.113.0/24",
"198.51.100.0/24",
"2001:db8::/32"
]
}
}
additional_blocked_domains (array, default: [])¶
Additional domain names to block beyond core protections.
Supports:
- Exact domain: "internal.example.com" - Blocks internal.example.com exactly
- Subdomain matching: "internal.example.com" - Also blocks api.internal.example.com, db.internal.example.com
- Wildcard patterns (NEW in v1.5.0): Use * and ? for flexible pattern matching
Wildcard Pattern Syntax:
- * - Match zero or more characters (within a domain component)
- ? - Match exactly one character
- Case-insensitive matching
Wildcard Pattern Examples:
{
"ssrf_protection": {
"additional_blocked_domains": [
// Exact domain + subdomain matching (traditional)
"internal.example.com",
// Wildcard patterns (NEW in v1.5.0)
"*.internal.com", // Block all .internal.com domains (api.internal.com, db.internal.com)
"admin.*", // Block admin.* with any suffix (admin.example.com, admin.local)
"*.corp.*", // Block all .corp. domains (api.corp.internal, db.corp.example.com)
"metadata.*", // Block all metadata.* endpoints (metadata.aws.com, metadata.google.internal)
"*.local", // Block all .local domains (test.local, dev.local)
"test?.example.com" // Block test1.example.com, test2.example.com, testa.example.com
]
}
}
Use Cases for Wildcard Patterns:
-
Block entire TLDs:
*.internal,*.local,*.corp -
Block subdomains:
*.admin.example.com,*.staging.* -
Block naming patterns:
metadata.*,admin.*,internal.* -
Enterprise policies: Block all internal domains with single pattern
Pattern Matching Behavior:
*.internal.commatches:- ✅
api.internal.com - ✅
db.internal.com - ✅
cache.internal.com - ❌
example.com -
❌
internal.com(no prefix) -
admin.*matches: - ✅
admin.example.com - ✅
admin.corp.internal - ✅
admin.local -
❌
api.example.com -
*.corp.*matches: - ✅
api.corp.internal - ✅
db.corp.example.com - ❌
corp.internal(no prefix) - ❌
api.example.com
Traditional Example (exact + subdomain matching):
{
"ssrf_protection": {
"additional_blocked_domains": [
"internal.example.com",
"admin.local",
"corp.internal"
]
}
}
allowed_domains (array, default: []) - NEW in v1.5.0¶
Issue #252: Domain allow-list to override additional_blocked_domains blocks while maintaining core protections.
Evaluation order (deny-first approach):
1. ✅ Check immutable core protections (metadata endpoints, dangerous schemes, private IPs)
2. ❌ Check deny-list (additional_blocked_domains)
3. ✅ Check allow-list (allowed_domains) - can override step 2, NOT step 1
Supports:
- Exact domain: "api.corp.internal"
- Subdomain matching: Allows v1.api.corp.internal if api.corp.internal is allowed
Use cases: 1. Internal APIs: Allow specific internal APIs while blocking other internal domains 2. Development servers: Allow specific dev/staging servers without allowing all localhost 3. Partner services: Allow specific partner domains on restricted networks 4. Granular control: Override broad domain blocks with specific exceptions
Example:
{
"ssrf_protection": {
"additional_blocked_domains": [
"api.corp.internal",
"admin.corp.internal",
"secret.corp.internal"
],
"allowed_domains": [
"api.corp.internal",
"public.corp.internal"
]
}
}
Result:
- ✅ http://api.corp.internal - ALLOWED (in allow-list, overrides deny-list)
- ✅ http://public.corp.internal - ALLOWED (in allow-list)
- ✅ http://v1.api.corp.internal - ALLOWED (subdomain of allowed domain)
- ❌ http://admin.corp.internal - BLOCKED (in deny-list, not in allow-list)
- ❌ http://secret.corp.internal - BLOCKED (in deny-list, not in allow-list)
⚠️ CRITICAL LIMITATION - Cannot Override Immutable Protections:
The allow-list CANNOT override these immutable core protections:
- ❌ Cloud metadata endpoints: 169.254.169.254, metadata.google.internal, metadata.goog, fd00:ec2::254
- ❌ Private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16
- ❌ IPv6 private: ::1, fc00::/7, fe80::/10
- ❌ Dangerous schemes: file://, gopher://, ftp://, data://, dict://, ldap://
Example - Immutable protections cannot be overridden:
{
"ssrf_protection": {
"allowed_domains": [
"metadata.google.internal", // ❌ Will NOT work - still blocked
"169.254.169.254" // ❌ Will NOT work - still blocked
]
}
}
Security best practices: - Use allow-lists sparingly and only for known-safe domains - Document the business reason for each allowed domain - Review allow-lists regularly and remove unused entries - Prefer network-level controls for critical infrastructure - Test in staging before deploying to production
allow_localhost (boolean, default: false)¶
Allow access to localhost (127.0.0.1, ::1) for local development.
Use case: Local development servers, testing environments.
Example:
Security warning: Only enable allow_localhost in development environments, never in production.
Examples¶
Example 1: AWS Metadata Endpoint Attack (BLOCKED)¶
# Attacker attempts to steal AWS credentials
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
AI Guardian blocks this with:
🚨 BLOCKED BY POLICY
🚨 SSRF ATTACK DETECTED
Detected threat:
• Reason: private IP address '169.254.169.254'
• URL: http://169.254.169.254/latest/meta-data/iam/security-credentials/
Example 2: Private Network Scanning (BLOCKED)¶
AI Guardian blocks this with:
🚨 BLOCKED BY POLICY
🚨 SSRF ATTACK DETECTED
Detected threat:
• Reason: private IP address '192.168.1.1'
• URL: http://192.168.1.1/admin
Example 3: File Access via file:// URL (BLOCKED)¶
AI Guardian blocks this with:
🚨 BLOCKED BY POLICY
🚨 SSRF ATTACK DETECTED
Detected threat:
• Reason: dangerous URL scheme 'file://'
• URL: file:///etc/passwd
Example 4: Public AWS Service (ALLOWED)¶
# Legitimate access to public S3 bucket
curl https://s3.amazonaws.com/my-bucket/file.txt
# Legitimate AWS CLI command
aws s3 ls s3://my-bucket/
AI Guardian allows these - public AWS services are NOT blocked.
Example 5: Local Development (ALLOWED with config)¶
With allow_localhost: true:
AI Guardian allows this when allow_localhost is enabled.
Without allow_localhost (default), this would be blocked.
False Positives¶
SSRF protection is designed to minimize false positives:
NOT Blocked (Legitimate Use)¶
✅ Public IP addresses:
- curl http://8.8.8.8 (Google DNS)
- curl https://1.1.1.1 (Cloudflare DNS)
✅ Public AWS services:
- curl https://s3.amazonaws.com/bucket/file.txt
- aws ec2 describe-instances
- aws s3 ls
✅ HTTPS URLs to public domains:
- curl https://api.github.com/repos
- wget https://releases.ubuntu.com/22.04/ubuntu-22.04.3-desktop-amd64.iso
✅ Commands without URLs:
- ls -la /var/log
- grep "error" /tmp/app.log
- find . -name "*.py"
Blocked (Security Threats)¶
❌ Private network access:
- curl http://10.0.0.1
- wget http://192.168.1.1/admin
❌ Metadata endpoints:
- curl http://169.254.169.254/latest/meta-data/
- curl http://metadata.google.internal/
❌ Dangerous schemes:
- curl file:///etc/passwd
- curl gopher://internal.server
❌ Localhost (by default):
- curl http://localhost:8080
- wget http://127.0.0.1:3000
Legitimate AWS Access vs Attacks¶
Key distinction: IP address vs domain name
Public AWS Services (ALLOWED)¶
# ✅ Public S3 endpoint (domain name, resolves to public IP)
curl https://s3.amazonaws.com/my-bucket/file.txt
# ✅ AWS CLI (uses public AWS APIs)
aws ec2 describe-instances
aws s3 ls s3://my-bucket/
Metadata Endpoints (BLOCKED)¶
# ❌ AWS metadata endpoint (private IP, credential theft)
curl http://169.254.169.254/latest/meta-data/
# ❌ Direct access to instance metadata
curl http://169.254.169.254/latest/user-data
Why the difference? - Public AWS services use public domain names (s3.amazonaws.com) that resolve to public IPs - Metadata endpoints use private IP address (169.254.169.254) for instance-local access only - Legitimate AWS usage never requires accessing 169.254.169.254 from Bash commands
Comprehensive SSRF Protection with OpenShell¶
For production deployments, ai-guardian's pattern-based SSRF detection should be complemented with runtime sandboxing using OpenShell.
Why OpenShell?¶
OpenShell provides real network isolation via policy-driven sandboxing: - Intercepts ALL outbound network calls (not just command strings) - Policy engine operates from application layer to kernel - Hot-reloadable policies without container restarts - Works with Claude Code, GitHub Copilot, and other AI agents
Architecture Comparison¶
ai-guardian (Hook-Based):
User → PreToolUse Hook → Pattern Match → Block if dangerous URL
↓
Tool Executes → ❌ Cannot see internal network calls
OpenShell (Runtime Sandbox):
User → Tool Executes → Network Call → Policy Engine → Block if violates policy
↑
Intercepts at kernel level
Example Policy¶
OpenShell uses declarative YAML for network policies:
# openshell-github-readwrite-policy.yaml
network:
outbound:
# Block metadata endpoints
- action: deny
destination: "169.254.169.254"
# Block private IPs
- action: deny
destination: "10.0.0.0/8"
- action: deny
destination: "172.16.0.0/12"
- action: deny
destination: "192.168.0.0/16"
# Allow specific public APIs
- action: allow
destination: "api.github.com"
methods: [GET, POST]
- action: allow
destination: "*.googleapis.com"
# Default deny
- action: deny
destination: "*"
Setup¶
# Install OpenShell
docker pull ghcr.io/nvidia/openshell:latest
# Run agent in OpenShell sandbox
openshell run --policy openshell-github-readwrite-policy.yaml -- claude-code
Defense in Depth Strategy¶
Layer 1: ai-guardian (IDE hooks) - Catches obvious mistakes in Bash commands - Fast, lightweight pattern matching - Educational value
Layer 2: OpenShell (Runtime sandbox) - Comprehensive network isolation - Policy-driven enforcement - Catches everything ai-guardian misses
Layer 3: Infrastructure (Network controls) - Firewall egress rules - VPC/subnet isolation - Cloud provider network policies
When to Use OpenShell¶
Required for: - ✅ Production agent deployments - ✅ Zero-trust environments - ✅ Compliance requirements (SOC 2, HIPAA) - ✅ Multi-tenant systems
Optional for: - ⚠️ Local development (overhead acceptable) - ⚠️ High-security development teams - ⚠️ Testing untrusted MCP servers
Learn More¶
- OpenShell GitHub
- OpenShell Documentation
- Compatible with: Claude Code, GitHub Copilot, OpenCode
Edge Cases and FAQs¶
Q: Can I disable SSRF protection for specific commands?¶
A: Not currently. SSRF protection applies to all Bash commands. Use action modes instead:
- "action": "warn" - Shows warning but allows execution
- "action": "log-only" - Logs but doesn't notify user
Q: What about DNS rebinding attacks?¶
A: AI Guardian does NOT perform DNS resolution (by design). This avoids: - Performance overhead - Network dependencies - TOCTOU (Time-of-Check-Time-of-Use) issues
This means a public domain that resolves to a private IP would bypass protection. This is a known limitation. For complete protection, combine with: - Network egress filtering - DNS filtering - Runtime monitoring
Q: Can I allow specific private IPs?¶
A: Core protections cannot be disabled. However, you can use action modes:
- Set "allow_localhost": true to allow localhost specifically
- Set "action": "warn" globally and approve on a case-by-case basis
Q: What about IPv6 metadata endpoints?¶
A: Fully supported. AI Guardian blocks:
- fd00:ec2::254 (AWS IPv6 metadata)
- All IPv6 private ranges (fc00::/7, fe80::/10)
- IPv6 loopback (::1)
Q: Performance impact?¶
A: <1ms overhead per Bash command. URL extraction and IP validation are highly optimized.
Q: Can attackers bypass this?¶
Known bypass vectors: - DNS rebinding (domain resolves to private IP) - URL redirects (server redirects to metadata endpoint) - URL shorteners (obscure destination)
Mitigations: - AI Guardian blocks direct access (first line of defense) - Combine with network egress filtering (second line) - Use runtime monitoring (third line)
Credits¶
SSRF protection inspired by: - Hermes Security Framework: https://github.com/fullsend-ai/experiments/tree/main/hermes-security-patterns - Validated against real-world SSRF attack payloads from Hermes testing
Related Documentation¶
- README.md - Main documentation
- CHANGELOG.md - Version history
- Tool Policy - Permission system
- Prompt Injection - Prompt injection detection
Security Notes¶
Defense in Depth: SSRF protection is ONE layer of security. Combine with: - ✅ Network egress filtering - ✅ DNS filtering - ✅ Runtime monitoring - ✅ Principle of least privilege - ✅ Code review
Pattern-Based Filtering Limitations: - Hook-based: Only inspects command strings and tool parameters - Cannot see MCP server internal network calls - Cannot intercept runtime network traffic - Does not perform DNS resolution (by design) - Cannot detect URL redirects during execution - Cannot detect DNS rebinding attacks - Cannot detect dynamic URL construction inside tools
What This Means: - ai-guardian catches obvious SSRF attempts in command strings - It does NOT provide comprehensive network security - For production deployments, use network-level controls and runtime sandboxing (see OpenShell above)
Fail-Closed: SSRF protection fails closed on errors - if URL parsing fails, the command is blocked.
No Warranty: This software is provided "AS IS" under the Apache 2.0 License.