如何修复服务器端请求伪造?

wen 网络安全 81

如何修复服务器端请求伪造(SSRF)?完整防御指南与实战问答

目录导读

  1. 什么是SSRF漏洞?核心危害解析
  2. SSRF攻击的常见场景与触发点
  3. 修复SSRF的7大核心防御策略
  4. 代码级修复示例(Python/Java/Node.js)
  5. 常见问题问答(FAQ)
  6. 构建纵深防御体系

什么是SSRF漏洞?核心危害解析

问:SSRF攻击到底是如何工作的?

如何修复服务器端请求伪造?

答:服务器端请求伪造(Server-Side Request Forgery,SSRF)是一种攻击者利用服务器作为代理,向内部或外部系统发起恶意请求的漏洞,当Web应用允许用户控制服务器发出的请求目标(如URL、IP、端口)且未做严格校验时,攻击者就能让服务器访问本不该暴露的内部资源,典型危害包括:

  • 内网扫描与数据窃取:攻击者让服务器请求http://192.168.1.1/admin,绕过防火墙直接访问内部管理后台。
  • 服务端敏感信息泄露:利用file://协议读取服务器本地文件,如file:///etc/passwd
  • 云服务元数据窃取:在AWS/Azure/GCP环境中,通过http://169.254.169.254/latest/meta-data/获取临时凭证。
  • 端口扫描与协议欺骗:利用服务器对内部Redis、MySQL等服务的未授权访问权限。

根据OWASP 2021年Top 10,SSRF已上升至第10位,且在云原生架构中尤其致命。


SSRF攻击的常见场景与触发点

问:我的应用在哪些地方容易引入SSRF?

答:以下功能常是SSRF“重灾区”:

  1. URL抓取功能:用户输入一个网址,服务器去下载该网页内容(如爬虫、预览生成、图片下载)。

    # 危险代码:直接使用用户输入的url进行请求
    response = requests.get(user_input_url)
  2. 跳转或重定向处理:用户提供的目标URL被服务器访问后返回结果,如/redirect?target=http://evil.com

  3. 文件处理与导入:服务器解析用户指定的远程资源,如CSV导入、RSS订阅、Webhook回调。

  4. 内部服务调用:通过URL参数指定要调用的API端点,如/api/proxy?url=http://internal-service:8080/status

  5. 云原生环境特殊场景:Kubernetes Pod允许被请求http://metadata.google.internal等元数据服务。

关键点:任何「服务器作为客户端去访问用户可控目标」的场景,都可能存在SSRF。


修复SSRF的7大核心防御策略

问:我应该从哪些层面修复SSRF?

答:修复SSRF需要“白名单+协议限制+网络隔离”多重防线:

策略1:严格白名单校验(最有效)

  • 只允许预定义的、可信的域名/IP列表,拒绝所有非白名单请求。
  • 实现方式:在前端和后端同时校验,使用正则或已知域名库匹配。
  • 注意:不要使用黑名单(如屏蔽0.0.1),攻击者可绕过(如使用0x7f000001[::1]或短域名localtest.me)。

策略2:禁用危险协议

  • 允许的协议限制为https://http://,禁止file://ftp://dict://gopher://等。
  • 在Python中可使用urlparse并检查scheme字段。

策略3:IP地址过滤与DNS解析验证

  • 获取用户输入的域名后,解析其真实的IP地址,禁止私有IP段(如10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8)。
  • 防止DNS rebinding攻击:解析IP后再次验证,或使用安全DNS库(如dnspythonresolve_with_nameservers)。

策略4:强制使用外部DNS服务器

  • 避免使用内部DNS解析,防止攻击者利用内部域名解析到内网地址。
  • 设置超时时间(如3秒),防止慢速攻击耗尽连接。

策略5:网络层隔离(最彻底)

  • 将应用服务器放在独立的安全组/子网中,禁止出站到内网地址
  • 使用SaaS服务(如Cloudflare Workers)或正向代理,只允许访问特定的外部端点。
  • 在Kubernetes中使用NetworkPolicy限制Pod对元数据服务的访问(如阻止254.169.254)。

策略6:限制请求速率与端口

  • 只允许访问标准端口(如80、443),禁止内部服务常用端口(如3306、6379)。
  • 设置请求超时和最大响应体大小,防止数据泄露。

策略7:使用安全的类库与框架

  • 很多现代框架(如Django的validators.URLValidator)可以限制协议和域名。
  • 在Node.js中使用ssrf-req-filter@financial-times/n-fetch等库自动防御。

代码级修复示例(Python/Java/Node.js)

Python(Flask)修复示例

from urllib.parse import urlparse
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# 白名单域名
WHITELIST_HOSTS = ["api.example.com", "images.example.com"]
# 允许的协议
ALLOWED_SCHEMES = ["http", "https"]
# 拒绝私有IP
PRIVATE_IP_RANGES = ["10.", "172.16.", "192.168.", "127.", "0.", "169.254."]
def is_safe_url(url):
    parsed = urlparse(url)
    # 检查协议
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False
    # 检查域名白名单
    if parsed.hostname not in WHITELIST_HOSTS:
        return False
    # 检查解析后的IP是否私有
    try:
        ip = socket.gethostbyname(parsed.hostname)
        for prefix in PRIVATE_IP_RANGES:
            if ip.startswith(prefix):
                return False
    except:
        return False
    return True
@app.route('/fetch', methods=['POST'])
def fetch_url():
    url = request.json.get('url')
    if not is_safe_url(url):
        return jsonify({"error": "Invalid URL"}), 400
    # 设置超时和重定向限制
    resp = requests.get(url, timeout=3, allow_redirects=False)
    return jsonify({"content": resp.text})

Java(Spring Boot)修复示例

import java.net.URI;
import java.net.InetAddress;
import org.springframework.web.client.RestTemplate;
public class SafeUrlFetcher {
    private static final String[] ALLOWED_HOSTS = {"api.example.com"};
    private static final String[] PRIVATE_RANGES = {"10.", "172.16.", "192.168."};
    public boolean isValidUrl(String urlString) {
        try {
            URI uri = new URI(urlString);
            // 限制协议
            String scheme = uri.getScheme();
            if (!"https".equals(scheme) && !"http".equals(scheme)) return false;
            // 检查域名白名单
            String host = uri.getHost();
            boolean hostOk = false;
            for (String allowed : ALLOWED_HOSTS) {
                if (allowed.equals(host)) { hostOk = true; break; }
            }
            if (!hostOk) return false;
            // 解析IP并拒绝私有
            InetAddress addr = InetAddress.getByName(host);
            String ip = addr.getHostAddress();
            for (String range : PRIVATE_RANGES) {
                if (ip.startsWith(range)) return false;
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    public String fetchContent(String url) {
        if (!isValidUrl(url)) throw new SecurityException("Blocked URL");
        RestTemplate rest = new RestTemplate();
        return rest.getForObject(url, String.class);
    }
}

Node.js(Express)修复示例

const axios = require('axios');
const { URL } = require('url');
const dns = require('dns').promises;
const PRIVATE_IPS = ['10.', '172.16.', '192.168.', '127.', '0.', '169.254.'];
async function isSafeUrl(urlString) {
    try {
        const parsed = new URL(urlString);
        // 限制协议
        if (!['http:', 'https:'].includes(parsed.protocol)) return false;
        // 白名单域名
        const allowed = ['api.example.com', 'cdn.example.com'];
        if (!allowed.includes(parsed.hostname)) return false;
        // DNS解析并过滤私有IP
        const addresses = await dns.resolve4(parsed.hostname);
        for (const ip of addresses) {
            for (const prefix of PRIVATE_IPS) {
                if (ip.startsWith(prefix)) return false;
            }
        }
        return true;
    } catch (e) {
        return false;
    }
}
app.post('/fetch', async (req, res) => {
    const url = req.body.url;
    if (!(await isSafeUrl(url))) {
        return res.status(400).json({error: 'Invalid URL'});
    }
    const response = await axios.get(url, { timeout: 3000, maxRedirects: 0 });
    res.json({content: response.data});
});

常见问题问答(FAQ)

问1:使用黑名单屏蔽127.0.0.1就够了吗?

答:远远不够,攻击者可以使用以下变体绕过:

  • 十进制IP:2130706433(即127.0.0.1的十进制)
  • 短域名:localtest.me解析到0.0.1spoofed.burpcollaborator.net
  • IPv6:[::1]0:0:0:0:0:0:0:1
  • URL混淆:http://127.1/(省略最后两位)
  • DNS重绑定:先返回合法IP,缓存过期后指向内网IP

正确做法:始终采用白名单+IP验证。

问2:在云环境(AWS/GCP/Azure)中如何防御?

答:除了代码层防御,必须进行网络隔离:

  • AWS:使用S3 VPC Endpoint、禁止实例访问254.169.254(除非必要)。
  • GCP:设置防火墙规则阻止对metadata.google.internal的访问。
  • Azure:在NSG中限制出站到元数据IP(254.169.254)。
  • 所有云平台:启用IMDSv2(元数据服务版本2),要求使用令牌和PUT请求。

问3:如果必须允许用户输入任意URL(如浏览器书签同步)怎么办?

答:这种情况最危险,建议采用:

  • 代理服务:使用外部沙箱(如Cloudflare Workers或独立的SSRF防御服务),将请求发往代理,代理返回内容而不暴露服务器IP。
  • 限制响应:只允许获取特定MIME类型(如图片、文本),放弃对风险内容的处理。
  • 强制HTTPS:且只允许访问公开可访问的IP范围(排除内网和云元数据)。

问4:如何自动化测试SSRF漏洞?

答:使用工具如:

  • Burp Suite Collaborator:提交URL到外部的监听服务,检测回调。
  • SSRFmap:自动化探测内网服务。
  • 自定义脚本:让服务器访问自己搭建的HTTP监听器,检查是否有请求到达。 注意:测试前需获得合法授权,避免侵入生产环境。

问5:SSRF与CSRF的区别是什么?

答:SSRF是服务器端发起请求,攻击目标是服务端的内部网络;CSRF是用户端发起请求,攻击目标是用户会话,SSRF危害更大,因为可能暴露整个内部基础设施。


构建纵深防御体系

修复SSRF不能只靠一个函数或一个配置,需要从多个维度构建安全防线:

  1. 代码层:严格执行白名单、限制协议、DNS验证、禁止私有IP。
  2. 网络层:通过安全组/防火墙/NetworkPolicy隔离服务器,禁止出站到内网和元数据。
  3. 云原生层:启用IMDSv2、使用VPC Endpoint、最小化元数据访问权限。
  4. 运行层:使用WAF规则(如Cloudflare的SSRF防御)、限制请求超时和响应大小。
  5. 监控层:记录所有出站请求异常(如请求私有IP、非标准端口),实时告警。

最后提醒:定期进行安全审计和渗透测试,更新依赖库,确保防御措施与最新攻击手法(如DNS重绑定、IP混淆)同步升级。

如果你正在开发一个涉及远程URL获取的功能,请将上述防御策略融入开发流程,而不是等到安全团队发现问题后再修复,安全的代码,胜过千万次补丁。

抱歉,评论功能暂时关闭!