flawopen.com/ssrf/Csharp
How HttpClient allows SSRF in ASP.NET Core, and how to enforce IP boundary assertions in SocketsHttpHandler.
Stellen Sie sich vor, Sie schicken einen Boten, um ein Paket in einem öffentlichen Geschäft abzuholen, geben ihm aber die Adresse des Tresors im Büro des Geschäftsführers. Da der Bote einen internen Ausweis besitzt, öffnet er den Tresor und händigt Firmengeheimnisse aus.
Web Application SecurityCWE-918 betroffen ist.CWE-918Defense-in-DepthDie Anwendung akzeptiert externe URL-Parameter für Webhooks, Avatare oder PDF-Voransichten.
Der Angreifer übergibt eine Ziel-URL auf Cloud-Metadaten oder interne Loopback-Adressen (z. B. http://169.254.169.254/latest/meta-data/).
Der HTTP-Client des Servers sendet die Anfrage aus der geschützten VPC, ohne die Ziel-IP gegen private Subnetzbereiche abzugleichen.
Der interne Metadatendienst vertraut der Server-Anfrage und liefert temporäre IAM-Schlüssel und Tokens zurück.
// 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");
}
}
}