SSRF 防御实战:从原理到 WAF 与代码层防护配置

适用场景

SSRF(Server-Side Request Forgery,服务端请求伪造)漏洞出现在 Web 应用需要从服务端发起 HTTP 请求的场景:URL 预览/截图生成、Webhook 回调、API 代理转发、图片/文件远程获取、第三方 OAuth 认证等。攻击者可利用该漏洞访问内网服务(如 Redis、MySQL、云元数据 API)或进行端口扫描。本文适用于 PHP/Python/Java/Go 等后端应用的安全加固,以及基于 Nginx 的 WAF 层防护。

前置条件

  • 一台运行 Linux 的服务器(本文以 Ubuntu 22.04 / CentOS 7+ 为例)
  • 已安装 Nginx 作为反向代理或 WAF
  • 具备 root 或 sudo 权限
  • 基本的正则表达式和 Nginx 配置知识

原理说明

SSRF 的核心成因是:应用根据用户输入的 URL 发起服务端 HTTP 请求,但未对目标地址做充分校验。攻击者可以构造 http://127.0.0.1:6379/http://169.254.169.254/latest/meta-data/(云厂商元数据)或 file:///etc/passwd 等特殊 URL,绕过前端限制直接访问内网资源。防御的核心思路是严格控制服务端发起的请求目标,建立多层校验机制:应用层验证 → DNS 解析校验 → 网络层拦截。

操作步骤

步骤一:URL 解析与白名单校验(代码层)

最基础的防御是白名单机制:只允许请求预先授权的域名或 IP 范围。以下 Python 示例展示了 URL 解析与校验逻辑:

import re, socket, ipaddress
from urllib.parse import urlparse

# 白名单域名列表
ALLOWED_DOMAINS = [
    'api.trusted.com',
    'cdn.trusted-cdn.com',
]
# 允许的 IP 范围(CIDR 格式)
ALLOWED_IPS = [
    ipaddress.ip_network('203.0.113.0/24'),
]

def validate_url(user_url):
    """校验请求 URL 是否安全"""
    try:
        parsed = urlparse(user_url)
    except Exception:
        return False, "URL 解析失败"

    # 1. 协议校验:只允许 http/https
    if parsed.scheme not in ('http', 'https'):
        return False, "不支持的协议: " + parsed.scheme

    # 2. 域名白名单
    hostname = parsed.hostname.lower()
    if hostname in ALLOWED_DOMAINS:
        return True, "域名在白名单中"

    # 3. 内网地址阻断
    try:
        addr = ipaddress.ip_address(hostname)
        if addr.is_private or addr.is_loopback or addr.is_link_local:
            return False, "内网地址被禁止: " + hostname
        if hostname == '169.254.169.254':
            return False, "云元数据地址被禁止"
    except ValueError:
        pass

    # 4. DNS 解析后再次校验
    try:
        ip = socket.gethostbyname(hostname)
        addr = ipaddress.ip_address(ip)
        if addr.is_private or addr.is_loopback or addr.is_link_local:
            return False, "DNS 解析指向内网地址: " + ip
        for net in ALLOWED_IPS:
            if addr in net:
                return True, "IP 在允许范围"
    except socket.gaierror:
        return False, "域名解析失败"

    return True, "校验通过"

# 测试用例
test_urls = [
    'http://127.0.0.1:6379/',
    'http://169.254.169.254/latest/meta-data/',
    'http://10.0.0.1/admin',
    'http://api.trusted.com/data',
    'http://evil.com/ssrf',
]
for url in test_urls:
    ok, reason = validate_url(url)
    print(f"{'OK' if ok else 'X'} URL: {url} - {reason}")

步骤二:302 重定向劫持防护

攻击者常利用重定向跳板绕过白名单:提供一个合法域名,该域名 302 跳转到内网地址。防御方案如下:

import requests

def safe_fetch(url, max_redirects=3, timeout=5):
    """安全 HTTP 请求,逐跳校验重定向目标"""
    ok, reason = validate_url(url)
    if not ok:
        raise ValueError("原始 URL 不通过: " + reason)

    current_url = url
    for i in range(max_redirects):
        r = requests.get(current_url, allow_redirects=False,
                         timeout=timeout, headers={'User-Agent': 'SafeFetch/1.0'})
        if r.status_code in (301, 302, 303, 307, 308):
            next_url = r.headers.get('Location', '')
            if not next_url:
                break
            ok, reason = validate_url(next_url)
            if not ok:
                raise ValueError("重定向目标不安全: " + next_url + " - " + reason)
            current_url = next_url
        else:
            return r.content

    r = requests.get(current_url, timeout=timeout,
                     headers={'User-Agent': 'SafeFetch/1.0'})
    return r.content

步骤三:Nginx WAF 层拦截

在 Nginx 反向代理层实施 SSRF 防护,作为第二道防线

# /etc/nginx/conf.d/ssrf_waf.conf

# 内网地址变量映射
map $http_host $is_internal_ip {
    default 0;
    "~^127\." 1;
    "~^10\." 1;
    "~^172\.(1[6-9]|2[0-9]|3[01])\." 1;
    "~^192\.168\." 1;
    "~^0\." 1;
    "~^169\.254\." 1;
    "~^::1$" 1;
    "~^fc00:" 1;
    "~^fe80:" 1;
}

server {
    listen 443 ssl;
    server_name your-api.com;

    if ($is_internal_ip) {
        return 403;
    }

    if ($arg_url ~* "^file:") {
        return 403;
    }

    location /proxy/ {
        limit_except GET HEAD { deny all; }
        proxy_connect_timeout 5s;
        proxy_read_timeout 5s;
        client_max_body_size 1k;
        proxy_pass $arg_url;
    }
}

步骤四:Java/Golang 安全实现参考

Java – 使用自定义 URLConnection 拦截器:

public class SSRFRequestFactory extends SimpleClientHttpRequestFactory {
    @Override
    protected void prepareConnection(HttpURLConnection conn, String method) {
        String host = conn.getURL().getHost();
        try {
            InetAddress addr = InetAddress.getByName(host);
            if (addr.isSiteLocalAddress() || addr.isLoopbackAddress()
                || addr.isLinkLocalAddress()) {
                throw new SecurityException("SSRF blocked: " + host);
            }
        } catch (UnknownHostException e) {
            throw new SecurityException("DNS resolve failed");
        }
        super.prepareConnection(conn, method);
    }
}

Golang – 使用自定义 Transport:

transport := &http.Transport{
    DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
        host, _, _ := net.SplitHostPort(addr)
        ip := net.ParseIP(host)
        if ip != nil {
            if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
                return nil, fmt.Errorf("SSRF blocked: %s", addr)
            }
        }
        return net.Dialer{}.DialContext(ctx, network, addr)
    },
}
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}

配置验证

# 1. 内网地址应被拒绝
curl -v "https://your-api.com/proxy/?url=http://127.0.0.1:6379/"
# 期望响应:403

# 2. 云元数据地址应被拒绝
curl -v "https://your-api.com/proxy/?url=http://169.254.169.254/latest/meta-data/"
# 期望响应:403

# 3. 白名单域名应正常通过
curl -v "https://your-api.com/proxy/?url=http://api.trusted.com/data"
# 期望响应:200 OK

# 4. 文件协议应被拒绝
curl -v "https://your-api.com/proxy/?url=file:///etc/passwd"
# 期望响应:403

常见问题

Q1: 为什么仅靠 URL 白名单还不够?

攻击者可利用 URL 语义解析差异绕过校验。例如 http://allowed-domain.com@127.0.0.1:6379(某些库会忽略 @ 前的部分)、http://127.0.0.1:6379#@allowed-domain.com(# 后的部分被忽略)或 DNS rebinding 攻击(第一次解析到合法 IP,第二次解析到内网 IP)。因此必须同时实施 DNS 二次校验、重定向逐跳检查和网络层拦截。

Q2: 如何处理需要访问大量动态 URL 的场景(如 URL 预览服务)?

这类场景建议采用代理隔离方案:在独立沙箱环境(Docker 容器 + 严格网络策略)中运行请求模块,容器仅开放外网访问能力,内网全部隔离。还可以使用无头浏览器 Puppeteer/Playwright 在隔离网络命名空间中渲染页面。应用层仍然需要实施域名黑名单和内容安全策略。

总结

SSRF 防御需要构建多层次防护体系:应用层 URL 白名单 + 内网 IP 阻断 + DNS 二次校验、网络层 Nginx WAF 拦截、基础设施层网络隔离。没有单一方案能 100% 防御 SSRF,组合多层策略可将风险降至最低。推荐定期使用开源工具 SSRFmap 进行自动化检测。