flawopen.com/ssrf/Csharp
How HttpClient allows SSRF in ASP.NET Core, and how to enforce IP boundary assertions in SocketsHttpHandler.
Imagine enviar um assistente de escritório para buscar um pacote em uma loja pública, mas fornecendo o endereço do cofre trancado na sala da gerência. Como o assistente tem um crachá interno, ele abre o cofre e entrega segredos da empresa.
Web Application SecurityCWE-918.CWE-918Defense-in-DepthO aplicativo recebe uma URL de usuário para baixar webhooks, buscar avatares ou gerar relatórios PDF.
O invasor fornece um endereço local ou do serviço de metadados de nuvem (ex: http://169.254.169.254/latest/meta-data/).
O cliente HTTP do servidor inicia a conexão de dentro da VPC privada sem validar o IP resolvido ou sub-rede.
O serviço interno de metadados confia no IP do servidor e retorna credenciais temporárias de IAM ou tokens de acesso.
// 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");
}
}
}