首页 / 帮助文档 / 网站漏洞防护之服务端请求伪造的内网地址黑名单

网站漏洞防护之服务端请求伪造的内网地址黑名单

服务端请求伪造(SSRF)攻击的核心原理,就是攻击者利用服务器端的请求功能,让服务器替自己去访问内部网络资源。而防御SSRF最直接有效的手段之一,就是建立一份严格的内网地址黑名单,把所有不允许服务器访问的内部IP段和域名全部拦截掉。简单来说,你需要在代码层面明确告诉服务器:这些地址你绝对不能去请求,一旦发现请求目标命中黑名单,直接拒绝并返回错误。这不是什么高深技术,而是一个必须落地执行的基础安全策略。

什么是SSRF攻击以及为什么需要内网地址黑名单

SSRF全称Server-Side Request Forgery,翻译过来就是服务端请求伪造。攻击者通过构造恶意请求,诱导服务器向攻击者指定的目标地址发起HTTP请求。因为服务器通常处于内网环境,拥有访问内部服务的权限,攻击者就可以借此扫描内网端口、读取内部敏感文件、甚至攻击内网中的其他系统。比如一个正常的图片URL预览功能,攻击者把URL改成http://192.168.1.1/admin,服务器就可能真的去访问内网的管理后台。

内网地址黑名单的作用就是从源头上堵住这个漏洞。你不可能让服务器访问所有地址,必须划定一个白名单或者黑名单。黑名单策略更适合初期快速部署,把所有已知的私有IP段、本地回环地址、云厂商元数据地址等全部列入禁止访问的列表。这样即使攻击者构造了内网地址的请求,服务器在发起请求之前就会先做校验,命中黑名单直接拦截。

哪些地址必须列入内网黑名单

构建黑名单不是随便写几个IP就行,你需要覆盖所有可能被利用的内网地址段。以下是必须包含的几大类:

第一类是私有IP地址段。根据RFC 1918标准,私有地址包括:10.0.0.0/8(10.0.0.0到10.255.255.255)、172.16.0.0/12(172.16.0.0到172.31.255.255)、192.168.0.0/16(192.168.0.0到192.168.255.255)。这三个段是企业内网最常用的地址范围,必须全部屏蔽。

第二类是本地回环地址。127.0.0.0/8整个段都是本地回环,尤其是127.0.0.1和127.0.0.0,攻击者经常用这个来探测服务器自身的服务。还有IPv6的::1也要封掉。

第三类是云厂商的元数据服务地址。比如阿里云的100.100.100.200、腾讯云的169.254.0.1、AWS的169.254.169.254。这些地址是云服务器获取实例信息的入口,一旦被访问,攻击者可以拿到AK/SK等敏感凭证。这类地址很多人会忽略,但危害极大。

第四类是链路本地地址。169.254.0.0/16这个段是自动分配的链路本地地址,在云环境和容器环境中非常常见,也必须加入黑名单。

第五类是其他特殊地址。比如0.0.0.0、255.255.255.255、以及各种保留地址段。另外还有一些看起来像公网但实际上是内网映射的地址,也需要根据实际业务情况判断是否加入。

黑名单的具体实现方式

黑名单的实现可以在应用层、框架层或者网关层来做。最推荐的是在应用层的请求发起逻辑中加入校验函数,因为这样最贴近业务,可以针对不同的功能模块设置不同的策略。

下面是一个Python实现的内网地址黑名单校验函数示例:

import ipaddress

# 内网地址黑名单列表
BLACKLIST_NETWORKS = [
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
    ipaddress.ip_network('127.0.0.0/8'),
    ipaddress.ip_network('169.254.0.0/16'),
    ipaddress.ip_network('0.0.0.0/8'),
    ipaddress.ip_network('100.64.0.0/10'),  # 运营商级NAT
    ipaddress.ip_network('198.18.0.0/15'),  # 基准测试用
]

# 云厂商元数据地址
BLACKLIST_HOSTS = [
    '100.100.100.200',
    '169.254.169.254',
    '169.254.0.1',
    'metadata.alibabacloud.com',
    'metadata.tencentyun.com',
]

def is_blocked_ip(target_ip):
    """检查目标IP是否在黑名单中"""
    try:
        ip = ipaddress.ip_address(target_ip)
        for network in BLACKLIST_NETWORKS:
            if ip in network:
                return True
        return False
    except ValueError:
        return False

def is_blocked_host(hostname):
    """检查主机名是否在黑名单中"""
    return hostname in BLACKLIST_HOSTS

def validate_request_target(url):
    """校验请求目标是否安全"""
    from urllib.parse import urlparse
    parsed = urlparse(url)
    hostname = parsed.hostname
    
    if is_blocked_host(hostname):
        return False, f"Host {hostname} is in blacklist"
    
    # 需要解析IP时再做进一步检查
    try:
        import socket
        ip = socket.gethostbyname(hostname)
        if is_blocked_ip(ip):
            return False, f"IP {ip} is in blacklist"
    except socket.gaierror:
        pass
    
    return True, "OK"

上面这段代码的逻辑很清晰:先定义黑名单网络段和黑名单主机名,然后提供两个校验函数,最后在请求入口统一调用validate_request_target进行判断。实际项目中,你还需要考虑DNS重绑定攻击的问题,也就是攻击者先让域名解析到公网IP通过校验,然后在请求时DNS变成内网IP。所以校验不能只做一次,要在实际发起请求的那一刻再次解析并校验。

黑名单策略的局限性和补充措施

光靠黑名单是不够的,它有几个明显的短板。第一,黑名单不可能穷举所有内网地址,特别是在大型企业网络中,可能存在你不知道的私有网段。第二,攻击者可以通过IP地址的各种变形来绕过,比如把192.168.1.1写成0xC0A80101(十六进制)、写成3232235777(十进制整数)、或者用IPv6的映射格式。第三,DNS重绑定攻击可以让黑名单校验失效。

所以黑名单必须配合其他措施一起使用。首先是白名单策略,只允许访问业务确实需要的域名和IP,这比黑名单更安全但维护成本更高。其次是在网络层面做隔离,服务器不应该有直接访问内网的能力,应该通过防火墙限制出站流量。再次是禁用不必要的协议,比如很多SSRF攻击会利用file://、gopher://、dict://等协议来读取本地文件或发起内部请求,这些协议在不需要的场景下应该直接禁用。

另外还有一个容易被忽视的点:不要信任用户输入的任何URL。即使你做了黑名单校验,也要对URL做严格的格式验证,防止攻击者通过URL编码、双重编码、大小写混写等方式绕过。比如把%31%32%37%2e%30%2e%30%2e%31这种编码形式还原后就是127.0.0.1,如果你的校验逻辑没有先做解码再校验,就会被绕过。

在不同技术栈中如何落地黑名单防护

不同的开发语言和框架有不同的实现方式,但核心逻辑是一样的。在Java的Spring框架中,你可以写一个拦截器或者过滤器,在HTTP请求发出之前做目标地址校验。在Node.js中,可以在axios或node-fetch的请求拦截器中加入校验逻辑。在PHP中,可以在cURL初始化之前用curl_setopt设置CURLOPT_RESOLVE或者在封装函数中加入校验。

以Java为例,这里给一个基于HttpClient的校验封装:

import java.net.InetAddress;
import java.net.URI;
import java.util.Arrays;
import java.util.List;

public class SSRFProtection {
    
    private static final List BLACKLIST_PREFIXES = Arrays.asList(
        "10.", "172.16.", "172.17.", "172.18.", "172.19.", 
        "172.20.", "172.21.", "172.22.", "172.23.", "172.24.",
        "172.25.", "172.26.", "172.27.", "172.28.", "172.29.",
        "172.30.", "172.31.", "192.168.", "127.", "169.254.",
        "100.100.100."
    );
    
    public static boolean isSafeUrl(String url) throws Exception {
        URI uri = new URI(url);
        String host = uri.getHost();
        
        // 检查主机名黑名单
        if (host.equals("metadata.alibabacloud.com") || 
            host.equals("metadata.tencentyun.com")) {
            return false;
        }
        
        // 解析IP并检查
        InetAddress address = InetAddress.getByName(host);
        String ip = address.getHostAddress();
        
        for (String prefix : BLACKLIST_PREFIXES) {
            if (ip.startsWith(prefix)) {
                return false;
            }
        }
        
        // 再次DNS解析确认(防DNS重绑定)
        InetAddress resolved = InetAddress.getByName(host);
        String resolvedIp = resolved.getHostAddress();
        for (String prefix : BLACKLIST_PREFIXES) {
            if (resolvedIp.startsWith(prefix)) {
                return false;
            }
        }
        
        return true;
    }
}

这段Java代码展示了一个基本的思路:先做前缀匹配快速过滤,然后做DNS解析拿到真实IP再次校验。实际生产环境中,建议使用成熟的IP库来做精确的网段判断,而不是简单的字符串前缀匹配,因为前缀匹配可能会误判一些合法的公网IP。

黑名单维护和持续更新的重要性

黑名单不是写一次就完事的,它需要持续维护。随着业务扩展,你的内网拓扑可能会变化,新的私有网段可能会加入,云厂商的元数据地址也可能更新。建议把黑名单配置做成可配置化的,放在配置文件或者配置中心里,而不是硬编码在代码中。这样运维人员可以在不重新部署的情况下更新黑名单规则。

同时建议建立监控和告警机制。当检测到有请求命中黑名单时,不要只是静默拒绝,应该记录日志并触发告警。因为频繁的黑名单命中可能意味着有人在尝试攻击,也可能意味着你的黑名单规则有问题误伤了正常业务。通过日志分析,你可以不断优化黑名单的准确性。

还有一点很关键:定期做安全审计和渗透测试。自己写的黑名单可能存在遗漏,通过专业的安全测试可以发现你没想到的绕过方式。特别是一些边缘场景,比如IPv6地址的处理、国际化域名的处理、URL中嵌入的特殊字符等,都需要在测试中验证。

总结:黑名单是基础但不是全部

内网地址黑名单是防御SSRF攻击的第一道防线,实施简单、见效快,适合所有有对外请求功能的系统。但它只是整体安全策略的一部分。真正安全的系统需要多层防御:网络隔离限制出站、应用层白名单加黑名单双重校验、协议层面禁用危险协议、DNS解析防重绑定、以及持续的监控和更新。把这些措施组合起来,才能真正把SSRF的风险降到最低。不要抱有侥幸心理觉得加了黑名单就万事大吉,安全是一个持续的过程,不是一次性的配置。