flawopen.com/Server-Side Request Forgery/C#
You build a web preview generator in C#. A user asks for a preview of 'http://169.254.169.254'. Without IP filtering, your Azure/AWS instance metadata keys are returned.
In .NET, httpClient.GetAsync(userUrl) fetches the requested address. Attackers use this to query cloud metadata or internal microservices in enterprise VPCs.
SSRF vulnerabilities in .NET applications deployed on Azure or AWS have resulted in unauthorized Managed Identity token exfiltration and complete subscription takeover.
CISA Cybersecurity Advisory & MITRE CVE repository.// VULNERABLE: Naive HttpClient usage with user-supplied URL
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class WebhookController : ControllerBase {
private readonly HttpClient _client = new HttpClient();
[HttpGet("test")]
public async Task<IActionResult> TestWebhook([FromQuery] string url) {
// Attacker input: http://169.254.169.254/metadata/v1/
var response = await _client.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
return Ok(content);
}
}
// HARDENED: SocketsHttpHandler with ConnectCallback IP validation
using System;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class WebhookController : ControllerBase {
private static readonly HttpClient SafeClient;
static WebhookController() {
var handler = new SocketsHttpHandler {
AllowAutoRedirect = false, // Block redirect bypasses
ConnectTimeout = TimeSpan.FromSeconds(3),
ConnectCallback = async (context, cancellationToken) => {
var entry = await Dns.GetHostEntryAsync(context.DnsEndPoint.Host, cancellationToken);
foreach (var ip in entry.AddressList) {
if (IPAddress.IsLoopback(ip) || isPrivateOrLinkLocal(ip)) {
throw new SocketException((int)SocketError.AccessDenied);
}
}
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(entry.AddressList[0], context.DnsEndPoint.Port, cancellationToken);
return new NetworkStream(socket, ownsSocket: true);
}
};
SafeClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) };
}
private static bool isPrivateOrLinkLocal(IPAddress ip) {
if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4();
byte[] bytes = ip.GetAddressBytes();
if (bytes[0] == 10) return true; // 10.0.0.0/8
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true; // 172.16.0.0/12
if (bytes[0] == 192 && bytes[1] == 168) return true; // 192.168.0.0/16
if (bytes[0] == 169 && bytes[1] == 254) return true; // 169.254.0.0/16 Link-Local
return false;
}
[HttpGet("test")]
public async Task<IActionResult> TestWebhook([FromQuery] string url) {
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme != "http" && uri.Scheme != "https")) {
return BadRequest("Invalid URL scheme");
}
try {
var response = await SafeClient.GetAsync(uri);
var content = await response.Content.ReadAsStringAsync();
return Ok(content);
} catch (Exception) {
return Forbid("Access to private/internal network rejected");
}
}
}
The ConnectCallback intercepts the socket creation immediately before the TCP connection. It resolves the host and rejects private or link-local addresses. Setting AllowAutoRedirect = false prevents redirect evasion.
Validating the URL before calling HttpClient.GetAsync leaves a DNS rebinding gap. The ConnectCallback approach resolves and connects to the exact same IP, eliminating TOCTOU.
When external webhooks must be fetched, denying private CIDRs (RFC 1918 + Link-Local) allows all public internet destinations while protecting internal infrastructure.
dotnet format analyzers
# Roslyn Rule CA3004
SocketsHttpHandler.ConnectCallback to inspect resolved IPsAllowAutoRedirect = falseAzure IMDS (169.254.169.254) requires an HTTP header: 'Metadata: true'. However, many internal Azure services (e.g. key vault firewalls) still rely on IP restriction.