CWE-918: Server-Side Request Forgery (SSRF) - C#
Overview
Server-Side Request Forgery (SSRF) allows attackers to make the server perform HTTP requests to arbitrary destinations, potentially accessing internal services, cloud metadata endpoints, or bypassing firewalls. Always validate URLs against an allowlist, block private IP ranges, and implement network segmentation.
Primary Defence: Validate URLs against an allowlist of permitted domains/IPs, block private IP ranges using IPAddress.IsLoopback() and RFC 1918 checks, and use SocketsHttpHandler to restrict DNS resolution.
Common Vulnerable Patterns
Direct URL Usage from User Input
// VULNERABLE - No validation on user-provided URL
using System.Net;
public class ImageFetcher
{
public byte[] FetchImage(string imageUrl)
{
// No validation - SSRF vulnerability!
using var client = new WebClient();
return client.DownloadData(imageUrl);
}
}
// Attack examples:
// http://localhost/admin
// http://169.254.169.254/latest/meta-data/iam/security-credentials/
// file:///C:/Windows/System.ini
Why this is vulnerable: Accepting user-provided URLs without validation allows attackers to make the server request internal resources (localhost, 169.254.169.254 cloud metadata, internal IPs), bypass firewalls, or access local files.
Unvalidated HttpClient Requests
// VULNERABLE - HttpClient without URL validation
using System.Net.Http;
public class WebhookHandler
{
private readonly HttpClient _httpClient = new HttpClient();
public async Task SendWebhookAsync(string webhookUrl, string data)
{
// No validation - SSRF vulnerability!
var response = await _httpClient.PostAsync(
webhookUrl,
new StringContent(data)
);
}
}
// Attack: webhookUrl = "http://internal-api.local/sensitive-endpoint"
Why this is vulnerable: HttpClient makes requests to any URL without validation, allowing attackers to scan internal networks, access cloud metadata endpoints, or pivot attacks through the server.
ASP.NET Core Controller Without Validation
// VULNERABLE - No URL validation in controller
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProxyController : ControllerBase
{
private readonly HttpClient _httpClient;
public ProxyController(IHttpClientFactory factory)
{
_httpClient = factory.CreateClient();
}
[HttpGet]
public async Task<IActionResult> Proxy([FromQuery] string url)
{
// No validation - SSRF vulnerability!
var response = await _httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
return Ok(content);
}
}
// Attack: /api/proxy?url=http://169.254.169.254/latest/meta-data/
Why this is vulnerable: Web API endpoints that fetch URLs from request parameters without validation enable SSRF attacks, allowing reconnaissance of internal services, access to cloud metadata, and potential data exfiltration.
Unvalidated Redirect
// VULNERABLE - Open redirect
[HttpGet("redirect")]
public IActionResult Redirect([FromQuery] string target)
{
// No validation - can redirect anywhere!
return Redirect(target);
}
// Attack: /redirect?target=http://internal-service.local/
Why this is vulnerable: Redirecting to user-controlled URLs without validation enables open redirect attacks and can trigger SSRF when combined with URL pre-fetching or link preview features.
Secure Patterns
URL Allowlist Validation
// SECURE - Validate URLs against allowlist
using System;
using System.Net;
using System.Collections.Generic;
public class SafeImageFetcher
{
private static readonly HashSet<string> AllowedHosts = new()
{
"api.example.com",
"cdn.example.com",
"images.example.com"
};
private static readonly HashSet<string> AllowedSchemes = new() { "https" };
public byte[] FetchImage(string imageUrl)
{
var validatedUri = ValidateUrl(imageUrl);
using var client = new WebClient();
return client.DownloadData(validatedUri);
}
private Uri ValidateUrl(string urlString)
{
if (!Uri.TryCreate(urlString, UriKind.Absolute, out Uri? uri))
{
throw new SecurityException("Invalid URL");
}
// Validate scheme
if (!AllowedSchemes.Contains(uri.Scheme.ToLowerInvariant()))
{
throw new SecurityException($"Invalid URL scheme: {uri.Scheme}");
}
// Validate host
string host = uri.Host.ToLowerInvariant();
if (!AllowedHosts.Contains(host))
{
throw new SecurityException($"Host not allowed: {host}");
}
// Block private IP ranges
if (IsPrivateIp(uri.Host))
{
throw new SecurityException("Private IP addresses not allowed");
}
return uri;
}
private bool IsPrivateIp(string host)
{
try
{
var addresses = Dns.GetHostAddresses(host);
foreach (var addr in addresses)
{
var bytes = addr.GetAddressBytes();
// Check for private IP ranges
if (bytes.Length == 4) // IPv4
{
// 10.x.x.x
if (bytes[0] == 10)
return true;
// 172.16.x.x - 172.31.x.x
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31)
return true;
// 192.168.x.x
if (bytes[0] == 192 && bytes[1] == 168)
return true;
// 127.x.x.x (loopback)
if (bytes[0] == 127)
return true;
// 169.254.x.x (link-local, AWS metadata)
if (bytes[0] == 169 && bytes[1] == 254)
return true;
}
// IPv6 loopback and link-local
if (addr.IsIPv6LinkLocal || addr.IsIPv6SiteLocal ||
IPAddress.IsLoopback(addr))
{
return true;
}
}
return false;
}
catch
{
// If DNS fails, block it
return true;
}
}
}
Why this works:
- Host allowlist: Prevents arbitrary URLs targeting internal services, cloud metadata (169.254.169.254), or localhost
- Scheme validation: Blocks
file://,ftp://,gopher://and other protocols that could read local files or exploit legacy services - DNS resolution defense:
Dns.GetHostAddresses()defeats domain-to-IP rebinding where malicious domain initially resolves to public IP (passing validation) then switches to private IP (127.0.0.1, 192.168.x.x) - IP range validation: Checks resolved IPs aren't private even after DNS tricks
- Fail-closed: Blocks on DNS errors to prevent TOCTOU (time-of-check-to-time-of-use) races
- Defense-in-depth: Scheme + host allowlist + IP range checks stops SSRF attacks exfiltrating metadata credentials or scanning internal networks
HttpClient with Validation
// SECURE - HttpClient with URL validation and restrictions
using System.Net.Http;
using System.Text.RegularExpressions;
public class SecureWebhookHandler
{
private readonly HttpClient _httpClient;
private static readonly Regex AllowedUrlPattern =
new(@"^https://([a-z0-9-]+\.)*example\.com/.*$", RegexOptions.IgnoreCase);
public SecureWebhookHandler()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = false, // Prevent redirect-based SSRF
MaxAutomaticRedirections = 0
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(10)
};
}
public async Task SendWebhookAsync(string webhookUrl, string data)
{
var validatedUri = ValidateWebhookUrl(webhookUrl);
var content = new StringContent(data);
var response = await _httpClient.PostAsync(validatedUri, content);
response.EnsureSuccessStatusCode();
}
private Uri ValidateWebhookUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
throw new SecurityException("URL cannot be empty");
}
// Check against allowlist pattern
if (!AllowedUrlPattern.IsMatch(url))
{
throw new SecurityException($"URL not allowed: {url}");
}
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri))
{
throw new SecurityException("Invalid URL");
}
// Only HTTPS
if (uri.Scheme != Uri.UriSchemeHttps)
{
throw new SecurityException("Only HTTPS allowed");
}
// Block private IPs
if (IsPrivateAddress(uri.Host))
{
throw new SecurityException("Private IP addresses not allowed");
}
return uri;
}
private bool IsPrivateAddress(string host)
{
try
{
var addresses = Dns.GetHostAddresses(host);
return addresses.Any(addr =>
addr.IsIPv6LinkLocal ||
addr.IsIPv6SiteLocal ||
IPAddress.IsLoopback(addr) ||
IsPrivateIPv4(addr));
}
catch
{
return true; // Block if DNS fails
}
}
private bool IsPrivateIPv4(IPAddress address)
{
if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
return false;
var bytes = address.GetAddressBytes();
return bytes[0] == 10 ||
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) ||
(bytes[0] == 192 && bytes[1] == 168) ||
bytes[0] == 127 ||
(bytes[0] == 169 && bytes[1] == 254);
}
}
Why this works:
- Redirect blocking:
AllowAutoRedirect = falseprevents attackers using allowed public URL redirecting to private endpoint (e.g.,https://example.com/redirect?to=http://localhost:6379) - Strict domain matching: Regex allowlist with subdomain constraints prevents typosquatting (example-com.evil.com) or path-based bypasses
- Timeout protection: Prevents SSRF-based DoS where attackers target slow internal services to exhaust server resources
- HTTPS-only validation: Protects credentials in transit and prevents downgrade attacks
- Comprehensive IP blocking: Covers all RFC 1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8), link-local (169.254.0.0/16) for cloud metadata and internal network access
- DNS exception handling: Prevents attackers using DNS timeouts to bypass validation
ASP.NET Core with Validation
// SECURE - ASP.NET Core controller with URL validation
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class SecureProxyController : ControllerBase
{
private readonly HttpClient _httpClient;
private readonly IUrlValidator _urlValidator;
public SecureProxyController(
IHttpClientFactory factory,
IUrlValidator urlValidator)
{
_httpClient = factory.CreateClient();
_urlValidator = urlValidator;
}
[HttpGet]
public async Task<IActionResult> Proxy([FromQuery] string url)
{
try
{
// Validate URL against allowlist and block private IPs
var validatedUri = _urlValidator.Validate(url);
var response = await _httpClient.GetAsync(validatedUri);
if (!response.IsSuccessStatusCode)
{
return StatusCode((int)response.StatusCode);
}
var content = await response.Content.ReadAsStringAsync();
return Ok(content);
}
catch (SecurityException ex)
{
return BadRequest(new { error = "Invalid URL" });
}
}
}
// IUrlValidator interface
// Implementation should follow URL validation patterns shown in sections 1 and 2:
// - Validate against allowlist of hosts
// - Check for allowed schemes (HTTPS only)
// - Block private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x)
// - Block link-local addresses (169.254.x.x)
// - Resolve DNS and validate resulting IPs
public interface IUrlValidator
{
Uri Validate(string url);
}
Why this works:
- Centralized logic:
IUrlValidatordependency injection makes SSRF protection testable and reusable across controllers - Flexible abstraction: Interface allows swapping implementations (stricter in production, relaxed in development) without code changes
- Connection pooling:
IHttpClientFactoryensures HttpClient instances pooled and configured consistently, preventing socket exhaustion - Global policies: Enforces consistent timeouts and handlers across all HTTP requests
- Information disclosure prevention:
SecurityExceptioncatch returns generic error message - attackers don't learn whether URL blocked due to private IP, DNS failure, or allowlist rejection - Appropriate error code: 400 Bad Request (client error) with generic "Invalid URL" message reveals nothing about failed validation, denying feedback for iterative probing
Protecting Cloud Metadata Endpoints
// SECURE - Block AWS/Azure/GCP metadata endpoints
public class MetadataProtection
{
private static readonly HashSet<string> BlockedHosts = new()
{
"169.254.169.254", // AWS/Azure metadata
"metadata.google.internal", // GCP metadata
"metadata",
"metadata.azure.com"
};
private static readonly HashSet<string> BlockedPaths = new()
{
"/latest/meta-data",
"/latest/user-data",
"/latest/dynamic",
"/computeMetadata/v1",
"/metadata/instance"
};
public void ValidateNotMetadata(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri))
{
throw new SecurityException("Invalid URL");
}
string host = uri.Host.ToLowerInvariant();
string path = uri.AbsolutePath;
// Block metadata service hostnames
if (BlockedHosts.Contains(host))
{
throw new SecurityException("Access to metadata service blocked");
}
// Block metadata paths
foreach (var blockedPath in BlockedPaths)
{
if (path.StartsWith(blockedPath, StringComparison.OrdinalIgnoreCase))
{
throw new SecurityException("Access to metadata endpoint blocked");
}
}
// Block link-local addresses
try
{
var addresses = Dns.GetHostAddresses(host);
foreach (var addr in addresses)
{
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
var bytes = addr.GetAddressBytes();
if (bytes[0] == 169 && bytes[1] == 254)
{
throw new SecurityException("Link-local addresses blocked");
}
}
}
}
catch (Exception ex) when (ex is not SecurityException)
{
throw new SecurityException("DNS resolution failed");
}
}
}
Why this works:
- Sensitive data exposure: Cloud metadata endpoints (AWS EC2, Azure VM, GCP Compute) expose IAM credentials, SSH keys, instance tags, user data scripts
- AWS metadata protection: Blocking 169.254.169.254 prevents most common SSRF attack vector - stealing temporary credentials
- GCP metadata defense:
metadata.google.internalhostname blocking addresses Google's different resolution approach - Path-based blocking: Catches requests using IP directly or bypassing host checks via open redirects
- Full link-local range: 169.254.0.0/16 check covers entire range, not just .254 - some clouds/custom services use other IPs in block
- DNS-based bypass prevention: Explicit metadata hostname blocking stops attackers registering domains resolving to 169.254.169.254
- Overlapping defenses: Combined host, path, and IP checks ensure even if one layer bypassed (e.g., allowed domain with open redirect), IP or path checks still block cloud credential access
Named HttpClient with Restrictions (.NET 6+)
// SECURE - Configure HttpClient with restrictions to prevent SSRF bypasses
using Microsoft.Extensions.DependencyInjection;
// Program.cs or Startup.cs
builder.Services.AddHttpClient("SecureClient", client =>
{
// Timeout prevents long-running requests to internal services
client.Timeout = TimeSpan.FromSeconds(10);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler
{
// AllowAutoRedirect = false prevents bypassing URL validation via redirects
// Example attack: attacker validates https://evil.com/safe which redirects to http://169.254.169.254/
AllowAutoRedirect = false,
// UseProxy = false prevents proxy-based bypasses
UseProxy = false,
// MaxAutomaticRedirections = 0 reinforces no redirect policy
MaxAutomaticRedirections = 0
};
})
.AddPolicyHandler(GetRetryPolicy());
// Service using named client
// IMPORTANT: HttpClient restrictions alone are NOT sufficient - must also validate URLs
public class ApiService
{
private readonly HttpClient _httpClient;
private readonly IUrlValidator _validator;
public ApiService(IHttpClientFactory factory, IUrlValidator validator)
{
_httpClient = factory.CreateClient("SecureClient");
_validator = validator;
}
public async Task<string> FetchDataAsync(string url)
{
// URL validation is REQUIRED - HttpClient config is defense in depth
var validatedUri = _validator.Validate(url);
var response = await _httpClient.GetAsync(validatedUri);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
Testing and Validation
SSRF vulnerabilities should be identified through:
- Static Analysis Tools: Use tools like SonarQube, Checkmarx, or Veracode to identify potential SSRF sinks
- Dynamic Application Security Testing (DAST): Tools like OWASP ZAP, Burp Suite, or Acunetix can test for SSRF by manipulating URL parameters
- Manual Penetration Testing: Test with internal IP addresses (127.0.0.1, 192.168.x.x), cloud metadata endpoints (169.254.169.254), and file:// protocols
- Code Review: Ensure all HTTP client usage includes URL validation against an allowlist
- Network Monitoring: Monitor outbound requests to detect unexpected internal network access