Learn to prevent Blind SSRF by securing your Cloud Metadata Service access. Discover how to harden Node.js and PHP HTTP clients to block unauthorized requests.
During a recent security audit of a legacy microservice, I traced a series of strange, intermittent connection timeouts back to a misconfigured webhook handler. Someone was probing our environment by injecting internal metadata IP addresses into an open URL field, attempting to trigger a Blind SSRF.
If you’re running workloads on AWS, GCP, or Azure, your instances are sitting on top of a goldmine: the metadata service. It’s a local endpoint that provides credentials, instance IDs, and configuration data to your code. If an attacker can force your HTTP client to reach 169.254.169.254, they don't necessarily need to see the response to know they've succeeded—they just need to trigger a timeout or an error that leaks information about your internal network topology.
Blind SSRF happens when an application makes a request to a user-supplied URL, but the attacker doesn't get the direct output. Instead, they look for side-channel signals: timing differences, error messages, or changes in HTTP status codes.
The Cloud Metadata Service is the primary target here. Because these services listen on non-routable link-local IP addresses (like 169.254.169.254 on AWS), they are reachable from within your container or VM. If your code uses a library like axios in Node.js or Guzzle in PHP without strict URL validation, you're essentially handing the keys to the kingdom to anyone who can provide an input string.
We initially tried using a simple regex to block the IP 169.254.169.254. It worked for about three hours until we realized the attacker could use decimal or hex representations of the IP, or even bypass the filter using DNS rebinding. That’s when I realized we needed a more robust approach to HTTP client security.
The most effective way to stop these attacks is to move away from "blacklisting" (blocking bad IPs) and toward "allow-listing" (only permitting known-good domains). You need to validate the destination before the request is even dispatched.
In Node.js, I prefer wrapping the native http module or using a library that allows custom agent configuration. If you’re already using axios, you can intercept the request before it leaves your server.
JAVASCRIPTconst axios = require(CE9178">'axios'); const url = require(CE9178">'url'); const allowedHosts = [CE9178">'api.trusted-partner.com', CE9178">'internal-service.local']; const secureClient = axios.create(); secureClient.interceptors.request.use((config) => { const parsedUrl = new url.URL(config.url); if (!allowedHosts.includes(parsedUrl.hostname)) { throw new Error(CE9178">'Security Error: Forbidden host'); } return config; });
This ensures that even if an attacker manages to pass http://169.254.169.254/latest/meta-data/ into your function, the request is killed before it hits the network stack. For a deeper dive into protecting your architecture, check out my previous guide on SSRF Prevention: Securing Cloud-Native Node.js Microservices.
In the PHP world, especially if you're working with Laravel HTTP Client: A Beginner’s Guide to Consuming External APIs, you should handle this via a dedicated middleware or a service wrapper.
PHPuse Illuminate\Support\Facades\Http; class SecureApiClient { public function get(string $url) { $host = parse_url($url, PHP_URL_HOST); if (!in_array($host, ['api.trusted-partner.com'])) { throw new \Exception("Unauthorized host: {$host}"); } return Http::get($url); } }
Even with application-level checks, a sufficiently complex environment demands Request Filtering at the network layer. If you're running on Kubernetes, don't rely solely on your application code.
I’ve started using NetworkPolicies to explicitly deny egress traffic to the metadata IP ranges. By default, pods in many clusters have broad outbound access. You can restrict this by applying a policy that drops traffic to 169.254.169.254.
Here is what that looks like in a standard K8s manifest:
YAMLkind: NetworkPolicy apiVersion: networking.k8s.io/v1 metadata: name: block-metadata-access spec: podSelector: {} policyTypes: - Egress egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 169.254.169.254/32
This acts as a safety net. If a developer forgets to implement host validation in a new microservice, the infrastructure will still block the attempt to reach the cloud metadata service.
The biggest mistake I made early on was thinking that host validation alone was enough. DNS rebinding—where an attacker points a domain they control to 169.254.169.254—can bypass simple hostname checks if the library resolves the IP once and then connects to it later.
Always ensure your HTTP client resolves the IP address and checks it against a list of forbidden ranges (like the link-local range) immediately before establishing the TCP connection. If you're using libraries like Got in Node.js, you can hook into the beforeRequest lifecycle event to perform this check.
I’m still experimenting with eBPF-based agents to perform these checks at the kernel level. It feels like the right direction for the future, as it removes the burden of security from the application developer entirely. Until then, keep your allow-lists tight and your network policies tighter.
Q: Why not just block 169.254.169.254 in my firewall? A: You should do that, but firewalls aren't always granular enough to distinguish between a legitimate internal service request and an SSRF attempt. Application-level validation is your first line of defense.
Q: Does this cover all cloud providers?
A: Most providers use 169.254.169.254. However, always check your specific cloud documentation for the exact CIDR range used by their metadata service, as some might use secondary ranges that you'll also need to filter.
Q: Is there a performance hit when validating every URL? A: Negligible. The overhead of a string comparison or a hash-map lookup is measured in microseconds—far faster than the latency of an external HTTP request.
JWT security depends on granular authorization scopes. Learn how to implement scope-based validation in Node.js and Laravel to prevent token over-privilege.
Read moreCommand injection in Node.js can lead to full server compromise. Learn how to stop it by moving away from shell execution and mastering secure input handling.