服务端请求伪造(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的风险降到最低。不要抱有侥幸心理觉得加了黑名单就万事大吉,安全是一个持续的过程,不是一次性的配置。
