You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过HttpClient发送Webhooks时防范SSRF漏洞?

用HttpClient发送Webhooks时防范SSRF漏洞的合理方案

问题背景

我需要让服务器应用向用户可控的HTTP(S) URL发送Webhooks(出站POST请求),但直接实现会存在SSRF风险——目标主机可能处于服务器所在的私有网络中。原本在经典.NET中,可通过HttpWebRequest的ServicePoint.BindIPEndPointDelegate回调获取要连接的IP地址,以此黑名单私有IP来防范;但迁移到.NET Core后,HttpWebRequest成为HttpClient的包装器,该回调完全失效,直接使用HttpClient也找不到能在连接前获取IP的回调。

尝试过提前解析域名到IP再发起请求,但存在诸多问题:需处理多个IP结果、存在ToC/ToU(检查与请求阶段解析结果不一致)问题,还要手动处理重定向以校验目标IP;另一种思路是解析域名后用IP作为连接目标、保留原Host头,但同样需手动处理重定向,实现复杂且脆弱;使用代理、防火墙等外部方案又不够简洁,还会增加基础设施成本与复杂度。

可行解决方案

1. 基于SocketsHttpHandler的ConnectCallback(推荐,.NET Core 3.0+)

在.NET Core 3.0及以上版本,SocketsHttpHandler提供了自定义连接逻辑的能力,可在连接建立前解析并校验IP,从根源避免SSRF:

var handler = new SocketsHttpHandler
{
    ConnectCallback = async (context, cancellationToken) =>
    {
        // 解析目标域名的IP列表
        var ipAddresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host, cancellationToken);
        // 过滤私有/保留IP地址
        var validIps = ipAddresses.Where(ip => !IsPrivateOrReservedIp(ip)).ToList();
        
        if (!validIps.Any())
        {
            throw new InvalidOperationException("目标地址属于私有/保留网络,禁止访问");
        }

        // 建立到合法IP的连接
        using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        await socket.ConnectAsync(new IPEndPoint(validIps[0], context.DnsEndPoint.Port), cancellationToken);
        return new NetworkStream(socket, ownsSocket: true);
    }
};

var httpClient = new HttpClient(handler);

配套的IP校验方法示例:

private static bool IsPrivateOrReservedIp(IPAddress ip)
{
    // 转换IPv6映射的IPv4地址
    if (ip.IsIPv4MappedToIPv6)
        ip = ip.MapToIPv4();

    var ipBytes = ip.GetAddressBytes();
    
    // 覆盖常见私有/保留IP段:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8等
    return ipBytes[0] == 10 ||
           (ipBytes[0] == 172 && ipBytes[1] is >= 16 and <= 31) ||
           (ipBytes[0] == 192 && ipBytes[1] == 168) ||
           ipBytes[0] == 127;
}

该方案的核心优势:

  • 连接阶段直接校验IP,彻底避免ToC/ToU问题
  • HttpClient自动处理重定向,所有重定向目标都会经过ConnectCallback校验
  • 基于.NET原生API实现,无外部依赖,逻辑简洁

2. HTTPS场景下的证书校验补充(辅助方案)

如果仅处理HTTPS请求,可以在证书校验阶段额外校验远程IP,作为安全兜底:

var handler = new HttpClientHandler
{
    ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) =>
    {
        if (msg.RequestMessage.Properties.TryGetValue("RemoteIpAddress", out var ipObj) && ipObj is IPAddress remoteIp)
        {
            if (IsPrivateOrReservedIp(remoteIp))
            {
                return false;
            }
        }
        // 保留原有证书校验逻辑
        return errors == SslPolicyErrors.None;
    }
};

注意:该方案是连接建立后才校验IP,存在一定风险,仅适合作为额外安全层,不单独使用。

3. 手动处理重定向(旧版本.NET Core兜底)

如果使用的是.NET Core 3.0以下版本,只能限制自动重定向,手动处理每个重定向请求并提前校验IP:

var handler = new HttpClientHandler
{
    AllowAutoRedirect = false
};
var httpClient = new HttpClient(handler);

var request = new HttpRequestMessage(HttpMethod.Post, userProvidedUrl);
// 设置请求内容等
var response = await httpClient.SendAsync(request);

// 手动处理重定向
while (response.StatusCode is HttpStatusCode.MovedPermanently or HttpStatusCode.Found or HttpStatusCode.TemporaryRedirect or HttpStatusCode.PermanentRedirect)
{
    var redirectUrl = response.Headers.Location;
    if (redirectUrl == null) break;

    // 解析重定向目标的IP并校验
    var ipAddresses = await Dns.GetHostAddressesAsync(redirectUrl.Host);
    if (ipAddresses.Any(IsPrivateOrReservedIp))
    {
        throw new InvalidOperationException("重定向目标属于私有网络,禁止访问");
    }

    request = new HttpRequestMessage(response.StatusCode is HttpStatusCode.TemporaryRedirect or HttpStatusCode.PermanentRedirect ? request.Method : HttpMethod.Get, redirectUrl);
    response = await httpClient.SendAsync(request);
}

该方案需处理多种重定向状态码,逻辑复杂且仍存在ToC/ToU问题,仅作为旧版本的临时解决方案。


内容的提问来源于stack exchange,提问作者CodesInChaos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 11:24:51