flawopen.com/ssrf/Csharp
How HttpClient allows SSRF in ASP.NET Core, and how to enforce IP boundary assertions in SocketsHttpHandler.
想象一下,你派办公室助理去公共快递站取包裹,却故意给他留了总经理办公室带锁保险箱的内部地址。因为助理佩戴着公司内部通行工牌,保安直接放行,他便打开保险箱将公司核心机密交给了你。
Web Application SecurityCWE-918.CWE-918CWE-918):Standard Common Weakness Enumeration classification for ssrf-csharp.Defense-in-Depth应用程序接受用户提交的外部 URL 用于拉取头像、Webhook 推送或生成网页预览。
攻击者输入指向内网回环地址或云厂商元数据接口(如 http://169.254.169.254/latest/meta-data/)的地址。
服务器内部 HTTP 客户端直接从受信任的私有 VPC 发起网络请求,未做 IP 白名单与私有地址段校验。
内部元数据服务信任来自同主机的请求,回传临时 IAM 访问凭据、Kubernetes Token 或管理后台内容。
// 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");
}
}
}