In the modern era of cloud computing and microservices, the “internal network” is no longer a safe haven. One of the most devastating vulnerabilities that has gained prominence over the last decade is Server-Side Request Forgery (SSRF). If you are a developer building Node.js applications that interact with third-party APIs, fetch remote images, or process user-provided URLs, your application might be a ticking time bomb.
Imagine a scenario where a user provides a URL for your server to generate a thumbnail. Instead of providing https://example.com/image.png, an attacker provides http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role. If your server is hosted on AWS, it might fetch its own administrative credentials and hand them over to the attacker on a silver platter. This isn’t a theoretical threat; it was the root cause of the infamous Capital One breach of 2019, which affected over 100 million customers.
In this comprehensive guide, we will dive deep into the mechanics of SSRF, explore how it manifests in Node.js environments, and provide a battle-tested roadmap to securing your infrastructure against both basic and advanced evasion techniques.
Understanding Server-Side Request Forgery (SSRF)
At its core, SSRF occurs when an attacker can influence a server-side application to make HTTP requests to an arbitrary domain or IP address. Essentially, the attacker uses the vulnerable server as a proxy to bypass firewalls, access internal services, and probe private networks that are otherwise inaccessible from the public internet.
Why SSRF is Particularly Dangerous in Node.js
Node.js is frequently used for building “BFFs” (Backends for Frontends), proxy layers, and microservices. Because Node.js is designed to be highly asynchronous and excels at handling I/O, developers often use it to aggregate data from multiple internal and external sources. This heavy reliance on network requests makes it a primary target for SSRF.
Consider the following features commonly implemented in Node.js:
- Webhooks: Sending data to a user-defined URL when an event occurs.
- PDF/Image Generators: Fetching remote assets to be rendered into a document.
- Import Tools: Downloading data from a URL provided by a user (e.g., “Import from CSV via URL”).
- URL Previewers: Generating metadata (OpenGraph) for a link.
The Mechanics: How an SSRF Attack Unfolds
To understand the defense, we must first understand the offense. There are two primary types of SSRF: Basic (Regular) SSRF and Blind SSRF.
1. Basic SSRF
In a basic SSRF attack, the application returns the response from the forged request back to the attacker. For example, if an attacker asks the server to fetch http://localhost:8080/admin and the server returns the HTML content of the admin panel, the attacker can successfully map out and interact with internal tools.
2. Blind SSRF
In a blind SSRF attack, the application does not return the response body. However, the attacker can still infer information based on the server’s behavior, such as response times, HTTP status codes (200 OK vs. 500 Error), or through “Out-of-Band” (OAST) techniques. For example, an attacker might trigger a request to their own server to see if the vulnerable server is “phoning home,” confirming the vulnerability exists.
Vulnerable Code: A Node.js Example
Let’s look at a typical, yet highly vulnerable, implementation of a URL proxying service in Node.js using the popular axios library.
const express = require('express');
const axios = require('axios');
const app = express();
/**
* VULNERABLE ENDPOINT
* This endpoint takes a 'url' query parameter and fetches it.
* There is NO validation, making it a perfect target for SSRF.
*/
app.get('/proxy', async (req, res) => {
const { url } = req.query;
try {
// The server blindly trusts the user-provided URL
const response = await axios.get(url);
res.send(response.data);
} catch (error) {
res.status(500).send('Error fetching the URL');
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
The Exploit: An attacker could call this endpoint as follows: /proxy?url=http://127.0.0.1:22. If the server is running an SSH daemon, the response might reveal the SSH version string, confirming that an internal port is open. More dangerously, on a cloud provider like AWS, the attacker could call /proxy?url=http://169.254.169.254/latest/meta-data/ to steal instance metadata.
Advanced Evasion Techniques
Many developers try to fix SSRF with simple regex or blocklists. However, attackers have numerous ways to bypass naive filters.
1. IP Encoding
Instead of using 127.0.0.1, an attacker can use different formats that many filters fail to recognize:
- Decimal:
http://2130706433 - Octal:
http://017700000001 - Hexadecimal:
http://0x7f000001
2. DNS Rebinding Attacks
This is one of the most sophisticated SSRF techniques. The attacker controls a domain (e.g., evil.com). They configure their DNS server to respond with a very short TTL (Time To Live), say 1 second.
- The application validates the domain. The DNS server returns a safe IP (e.g.,
1.1.1.1). - The application’s validation logic passes.
- The application then makes the actual request. Because the TTL has expired, it performs a second DNS lookup.
- This time, the attacker’s DNS server returns
127.0.0.1. - The application connects to its own internal services, bypassing the initial check.
3. Redirects (3xx)
If your HTTP client follows redirects automatically (which axios and node-fetch do by default), an attacker can provide a URL to a site they control that issues a 302 Redirect to an internal IP. The validation logic might check the first URL, but the actual request follows the redirect to the forbidden target.
Step-by-Step Instructions for Secure Request Handling
To properly defend against SSRF in Node.js, we need a multi-layered approach. Follow these steps to secure your application.
Step 1: Implement an Allowlist (Not a Blocklist)
Never try to block “bad” IPs like 127.0.0.1. There are too many variations. Instead, maintain a list of trusted domains or IP ranges that your application is permitted to access.
Step 2: Validate the Protocol
Ensure that the URL starts with http: or https:. Attackers might try to use other protocols like file:///etc/passwd, dict://, or gopher:// to interact with the file system or other services.
Step 3: Resolve and Validate IPs at the Network Level
To prevent DNS Rebinding, you must resolve the domain to an IP address once, validate that IP, and then perform the request directly to that IP address while maintaining the original Host header.
Step 4: Use a Specialized Library
Don’t reinvent the wheel. Libraries like ssrf-filter or custom http.Agent implementations can help enforce these rules.
const axios = require('axios');
const ipaddr = require('ipaddr.js');
const dns = require('dns').promises;
/**
* A robust function to validate a URL against SSRF
* @param {string} userUrl
* @returns {Promise<string|null>} The validated IP or null if unsafe
*/
async function validateAndResolve(userUrl) {
try {
const parsedUrl = new URL(userUrl);
// 1. Protocol Validation
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
// 2. Resolve DNS
const addresses = await dns.lookup(parsedUrl.hostname);
const ip = addresses.address;
// 3. IP Validation (Check if internal/private)
if (isPrivateIP(ip)) {
throw new Error('Access to private IP addresses is forbidden');
}
return ip;
} catch (err) {
console.error('SSRF Validation failed:', err.message);
return null;
}
}
function isPrivateIP(ip) {
const addr = ipaddr.parse(ip);
const range = addr.range();
// Ranges like 'loopback', 'private', 'linkLocal', 'unspecified' are dangerous
const forbiddenRanges = ['loopback', 'private', 'linkLocal', 'unspecified', 'multicast'];
return forbiddenRanges.includes(range);
}
Hardening the HTTP Client: The Custom Agent Approach
The most reliable way to prevent SSRF in Node.js is to use a custom http.Agent and https.Agent that hooks into the socket creation process. This ensures that even if a redirect occurs, the new target is still validated before a connection is established.
const http = require('http');
const https = require('https');
const { URL } = require('url');
const ipaddr = require('ipaddr.js');
/**
* createSafeAgent creates a wrapper that prevents connections to private IPs.
* This is effective against DNS rebinding and redirect-based SSRF.
*/
function createSafeAgent(ProtocolAgent) {
return class SafeAgent extends ProtocolAgent {
createConnection(options, callback) {
const host = options.host;
// Check if the host is already an IP address
if (ipaddr.isValid(host)) {
if (isPrivateIP(host)) {
return callback(new Error('Restricted IP address'));
}
}
// For hostnames, the check happens at the socket level
const socket = super.createConnection(options, callback);
socket.on('lookup', (err, address) => {
if (isPrivateIP(address)) {
socket.destroy(new Error('Restricted IP address resolved'));
}
});
return socket;
}
};
}
const SafeHttpAgent = createSafeAgent(http.Agent);
const SafeHttpsAgent = createSafeAgent(https.Agent);
// Usage with Axios
axios.get('https://example.com/data', {
httpAgent: new SafeHttpAgent(),
httpsAgent: new SafeHttpsAgent()
});
Protecting Cloud Metadata Services
If your application runs on AWS, Azure, or GCP, protecting the metadata service is critical. Cloud providers have realized the risk of SSRF and introduced countermeasures:
AWS IMDSv2
AWS introduced Instance Metadata Service Version 2 (IMDSv2), which is session-oriented. It requires a PUT request to get a token before a GET request can be made to fetch metadata. This significantly mitigates SSRF because most SSRF vulnerabilities are restricted to simple GET requests without custom headers.
Action Item: Configure your EC2 instances to require IMDSv2 and disable IMDSv1 entirely.
Google Cloud (GCP)
GCP requires a special header: Metadata-Flavor: Google for all metadata requests. Unless your SSRF vulnerability allows the attacker to set custom headers, they cannot access GCP metadata.
Common Mistakes and How to Fix Them
1. Trusting the ‘Host’ Header
Mistake: Using the Host header from the incoming request to build internal URLs.
Fix: Use a hardcoded configuration for internal service discovery or an environment variable.
2. Insufficient Regex for IP Validation
Mistake: Using a regex like /^127\./ to block localhost.
Fix: Use a dedicated library like ipaddr.js that understands the full range of IPv4 and IPv6 addresses, including mapped addresses and different encodings.
3. Not Disabling Redirects
Mistake: Using default axios settings which follow up to 5 redirects.
Fix: Set maxRedirects: 0 if redirects are not strictly necessary for your feature, or use the custom agent approach shown above to validate each step of the redirect chain.
4. Ignoring IPv6
Mistake: Only blocking 127.0.0.1 but forgetting about [::1].
Fix: Always validate for both IPv4 and IPv6 address families.
Testing Your Application for SSRF
As a developer, you should proactively test your endpoints. Tools like Burp Suite or OWASP ZAP are excellent for this. Additionally, you can use “Request Bin” services like interactsh to test for Blind SSRF.
- Try fetching
http://localhost:portwith different common ports (22, 80, 443, 3000, 8080). - Try fetching cloud-specific metadata URLs.
- Try using a DNS Rebinding tool like
rbndr.usto see if your validation can be bypassed by a mid-flight IP swap.
Summary / Key Takeaways
- SSRF is a critical vulnerability that allows attackers to use your server as a proxy to attack internal systems or steal cloud credentials.
- Never trust user-supplied URLs. Always treat them as malicious input.
- Allowlists are superior to blocklists. Define exactly where your server is allowed to go.
- The network layer is the safest place to validate. Perform DNS resolution and validate the resulting IP address before the connection starts.
- Use IMDSv2 on AWS and equivalent security measures on other cloud providers to protect metadata.
- Don’t forget redirects and IPv6. Ensure your security logic accounts for the entire request lifecycle.
Frequently Asked Questions (FAQ)
1. What is the difference between SSRF and CSRF?
Cross-Site Request Forgery (CSRF) involves an attacker tricking a user’s browser into making a request to a site where the user is authenticated. Server-Side Request Forgery (SSRF) involves the application server itself making a request to a target chosen by the attacker. CSRF targets users; SSRF targets the server and its internal network.
2. Can a Web Application Firewall (WAF) stop SSRF?
A WAF can help by blocking known malicious payloads and patterns (like metadata URLs), but it is not a silver bullet. Attackers can use encoding or DNS rebinding to bypass WAF rules. Code-level validation is always necessary for robust defense.
3. Is SSRF only possible with HTTP?
No. While HTTP is the most common vector, SSRF can occur with any protocol the server-side library supports. This includes FTP, SMB, gopher, file, and more. This is why protocol validation (restricting to http/https) is a vital first step.
4. Does using a VPN or VPC protect me from SSRF?
Actually, being inside a VPC can make SSRF more dangerous. The SSRF vulnerability effectively places the attacker inside your private network, allowing them to bypass the perimeter security provided by the VPC or VPN. Secure code is required to protect the “soft underbelly” of your internal network.
5. Should I use a library or write my own validation?
Whenever possible, use established libraries like ssrf-filter or ipaddr.js. Handling every edge case of IP representation (hex, octal, IPv4-mapped IPv6) is complex and prone to errors if done manually.
