Skip to content

Prevent SSRF bypass in hostname suffix validation

pattern

Hostname validation using endsWith allows SSRF bypass via attacker-controlled domain with same suffix

securityssrfvalidationnode
16 views

Problem

A common pattern for validating URLs in server-side proxy routes uses endsWith to check the hostname against an allowlist. This is vulnerable to SSRF bypass because an attacker can register a domain that ends with the same string:

// Vulnerable validation
const allowedDomain = "bandsintown.com";

function isAllowed(url: string): boolean {
  const hostname = new URL(url).hostname;
  return hostname.endsWith(allowedDomain);
}

isAllowed("https://bandsintown.com/api/events");     // true (intended)
isAllowed("https://api.bandsintown.com/api/events");  // true (intended)
isAllowed("https://evil-bandsintown.com/api/events"); // true (SSRF bypass!)
isAllowed("https://attackerbandsintown.com/exfil");   // true (SSRF bypass!)

An attacker registers evil-bandsintown.com and uses your server as a proxy to make requests to their controlled infrastructure or internal services.

Solution

Option 1: Exact match plus dot-prefixed subdomain check (recommended)

function isAllowedHostname(url: string, allowedDomain: string): boolean {
  const hostname = new URL(url).hostname;
  return hostname === allowedDomain || hostname.endsWith(`.${allowedDomain}`);
}

isAllowedHostname("https://bandsintown.com/api", "bandsintown.com");       // true
isAllowedHostname("https://api.bandsintown.com/api", "bandsintown.com");   // true
isAllowedHostname("https://evil-bandsintown.com/api", "bandsintown.com");  // false

Option 2: Validate against a full origin allowlist

const ALLOWED_ORIGINS = new Set([
  "https://bandsintown.com",
  "https://api.bandsintown.com",
]);

function isAllowedOrigin(url: string): boolean {
  const { origin } = new URL(url);
  return ALLOWED_ORIGINS.has(origin);
}

Why It Works

endsWith('bandsintown.com') is a pure string operation with no understanding of DNS hierarchy. The string evil-bandsintown.com ends with bandsintown.com, so the check passes. Adding a dot prefix (.bandsintown.com) ensures that only actual subdomains match, because DNS labels are separated by dots. The exact match check (hostname === domain) handles the root domain case.

Option 2 is even stricter because it validates the full origin (scheme + host + port), which also prevents protocol downgrade attacks (e.g., http:// vs https://).

Context

  • OWASP classifies this as a Server-Side Request Forgery (SSRF) vulnerability
  • Commonly found in API proxy routes, webhook URL validation, and OAuth redirect URI checks
  • Also applies to startsWith checks on URL paths -- use a URL parser instead of string operations
  • Always use new URL() to parse URLs rather than regex or string splitting, which can be tricked by malformed URLs like https://evil.com@bandsintown.com
  • Consider also validating that the resolved IP address is not in a private range (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) to prevent DNS rebinding attacks
About this share
Contributormblode
Repositorymblode/shares
CreatedFeb 9, 2026
Environment
View on GitHub