flawopen.com/ssrf/Csharp
How HttpClient allows SSRF in ASP.NET Core, and how to enforce IP boundary assertions in SocketsHttpHandler.
Bayangkan mengirim kurir kantor untuk mengambil paket di toko umum, tetapi memberinya alamat brankas terkunci di ruang manajer. Karena kurir memiliki lencana akses internal, dia membuka brankas dan menyerahkan rahasia perusahaan kepada Anda.
Web Application SecurityCWE-918.CWE-918Defense-in-DepthAplikasi menerima parameter URL dari pengguna untuk mengunduh avatar, webhook, atau pratinjau dokumen.
Penyerang memasukkan alamat loopback atau layanan metadata cloud (misal: http://169.254.169.254/latest/meta-data/).
Klien HTTP server mengirim permintaan dari dalam VPC privat tanpa memeriksa rentang subnet privat.
Layanan metadata internal mempercayai permintaan dan mengembalikan token kredensial IAM sementara ke penyerang.
// 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");
}
}
}