flawopen.com/Server-Side Request Forgery/C#

Server-Side Request Forgery in C#

High Severity CWE-918
ELI5

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.

Key terms on this page
SocketsHttpHandler
The modern, high-performance HTTP socket handler in .NET Core / .NET 6+ with extensible connection callbacks.
IPAddress.IsLoopback
Determines whether an IPAddress belongs to the loopback subnet.

What's happening

In .NET, httpClient.GetAsync(userUrl) fetches the requested address. Attackers use this to query cloud metadata or internal microservices in enterprise VPCs.

Real-world impact

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 vs. fixed

VULNERABLE
// 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);
    }
}
FIXED
// 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");
        }
    }
}

Why the fix works

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.

Gotchas

DNS Rebinding in HttpClient

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.

Common misconceptions

"Using an IP allowlist is impractical"

When external webhooks must be fetched, denying private CIDRs (RFC 1918 + Link-Local) allows all public internet destinations while protecting internal infrastructure.

How to check if you're affected

dotnet format analyzers # Roslyn Rule CA3004

Prevention checklist

FAQ

How does Azure protect against SSRF?

Azure 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.

References