Chapter 05

CSRF 与 SSRF

理解跨站请求伪造与服务端请求伪造——两种利用信任关系的危险攻击

CSRF:跨站请求伪造

攻击原理

CSRF(Cross-Site Request Forgery,跨站请求伪造)利用的是:浏览器在发起跨站请求时会自动携带目标站点的 Cookie。攻击者诱骗已登录的受害者访问恶意页面,该页面向受害者已登录的网站发起请求,服务器看到合法的 Cookie 便执行了操作——而受害者根本不知道。

CSRF 攻击流程: ① 受害者登录 bank.com,获得 Session Cookie ② 受害者(未登出的情况下)访问 evil.com ③ evil.com 的页面包含隐藏表单: <form action="https://bank.com/transfer" method="POST"> <input name="to" value="attacker_account"> <input name="amount" value="10000"> </form> <script>document.forms[0].submit()</script> ④ 浏览器自动携带 bank.com 的 Cookie 发送 POST 请求 ⑤ bank.com 收到合法 Cookie,认为是受害者本人操作,执行转账

CSRF 的前提条件

CSRF 防御方案

方案一:CSRF Token(同步器令牌模式)

服务器为每个会话(或每个表单)生成一个随机、不可预测的 Token,嵌入表单中。提交时验证 Token 是否匹配。攻击者无法获取受害者页面上的 Token(同源策略阻止跨域读取),因此无法伪造含有效 Token 的请求。

# Flask 实现 CSRF Token(flask-wtf 库已内置)
import secrets
from flask import session, request

def generate_csrf_token():
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)  # 256-bit 随机
    return session['csrf_token']

def validate_csrf_token():
    token = request.form.get('csrf_token') or \
            request.headers.get('X-CSRF-Token')
    if not token or token != session.get('csrf_token'):
        abort(403)  # Token 不匹配,拒绝请求
<!-- 表单中嵌入 CSRF Token -->
<form method="POST" action="/transfer">
  <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
  <input name="amount" type="number">
  <button type="submit">转账</button>
</form>
// AJAX 请求中通过请求头携带 CSRF Token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken  // 自定义请求头跨站请求无法设置
  },
  body: JSON.stringify({ amount: 100, to: 'alice' })
});

方案二:SameSite Cookie(最简单有效)

现代浏览器支持的 SameSite Cookie 属性是防御 CSRF 最优雅的方案,无需修改应用逻辑。

# 推荐配置(防 CSRF + 保持可用性)
Set-Cookie: sessionid=abc123; SameSite=Lax; HttpOnly; Secure; Path=/

# SameSite=Strict 防护最强:
# - 从任何外部站点的链接、表单、iframe、AJAX 发起的请求都不携带 Cookie
# - 缺点:用户从外部链接跳入后是未登录状态(体验差)

# SameSite=Lax(推荐默认值):
# - 跨站 POST/iframe/图片/AJAX 不携带 Cookie(阻止 CSRF 攻击)
# - 用户点击链接导航时携带 Cookie(保持正常跳转体验)
# - 现代浏览器的默认值(不指定 SameSite 时)

方案三:Referer / Origin 头验证

检查请求的 RefererOrigin 头,确认请求来自同域。注意:

方案四:Double Submit Cookie

无需服务端存储 Token 的无状态防御方案:服务器生成随机值同时写入 Cookie 和表单字段,提交时验证两者相等。攻击者无法读取 Cookie(同源策略),因此无法在请求中附上相同的值。

2024 年最佳实践推荐

推荐组合使用:SameSite=Lax(或 Strict) 作为主要防御 + CSRF Token 作为深度防御。SameSite 已被所有现代浏览器支持,在 SameSite Cookie 生效的情况下,大多数 CSRF 攻击已被阻断。保留 CSRF Token 是为了覆盖旧浏览器和某些 SameSite 不生效的边缘场景(如子域名 Cookie)。

SSRF:服务端请求伪造

攻击原理

SSRF(Server-Side Request Forgery,服务端请求伪造)是指攻击者让服务器向攻击者指定的 URL 发起请求。当应用接收用户提供的 URL 并代为请求时(如"截图 URL 预览"、"导入远程文档"、Webhook 测试),如果没有限制目标地址,攻击者可以让服务器访问内网服务或云环境的元数据接口。

SSRF 攻击场景(云环境元数据接口): 攻击者请求: POST /api/preview {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/role"} 服务器处理: 1. 收到 URL,代为发起 HTTP 请求 2. 169.254.169.254 是 AWS EC2 实例元数据服务 3. 服务器在 AWS 内网,可以访问该 IP 4. 元数据服务返回该 EC2 实例的 IAM 临时凭证 攻击者获得: { "AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "..." } → 使用这些凭证调用 AWS API,获取 S3 数据等

SSRF 可以攻击哪些目标

云元数据接口
AWS: 169.254.169.254,GCP: metadata.google.internal,Azure: 169.254.169.254。可获取 IAM 凭证、用户数据、实例信息。Capital One 2019 年泄露事件的根因。
内网服务探测
扫描内网的 Redis(6379)、Elasticsearch(9200)、MongoDB(27017)、Kubernetes API(6443)等未加认证的服务,可能直接读取数据或执行命令。
文件读取(file://)
如果应用支持 file:// 协议,可读取服务器本地文件:file:///etc/passwdfile:///app/config.yaml(含数据库密码)。
内网 HTTP 服务
访问只对内网开放的管理界面(Jenkins、Grafana、内部 API),这些服务通常假设调用者是可信内网用户,缺乏认证。

SSRF 防御方案

方案一:URL 白名单验证

import urllib.parse
import ipaddress

ALLOWED_DOMAINS = {'api.example.com', 'cdn.example.com'}

def validate_url(url: str) -> bool:
    try:
        parsed = urllib.parse.urlparse(url)

        # 1. 只允许 HTTP/HTTPS 协议
        if parsed.scheme not in ('http', 'https'):
            return False  # 禁止 file://, gopher://, dict:// 等

        # 2. 域名白名单
        if parsed.hostname not in ALLOWED_DOMAINS:
            return False

        return True
    except:
        return False

def is_internal_ip(ip_str: str) -> bool:
    """检查 IP 是否是内网地址"""
    try:
        ip = ipaddress.ip_address(ip_str)
        return (
            ip.is_private or
            ip.is_loopback or
            ip.is_link_local or  # 169.254.0.0/16 云元数据
            ip.is_reserved or
            str(ip) == '0.0.0.0'
        )
    except:
        return True  # 解析失败视为危险

方案二:DNS 重绑定防护

DNS 重绑定(DNS Rebinding)是 SSRF 的绕过技术:域名首次解析返回合法 IP(通过白名单),之后 TTL 过期,攻击者将该域名解析到内网 IP。防御:在 URL 验证后,实际发起请求前再次解析并检查解析结果。

import socket
import requests

def safe_request(url: str):
    parsed = urllib.parse.urlparse(url)
    hostname = parsed.hostname

    # 解析所有 IP(处理多 A 记录)
    try:
        addrs = socket.getaddrinfo(hostname, None)
        for addr in addrs:
            ip = addr[4][0]
            if is_internal_ip(ip):
                raise ValueError(f"SSRF 防护:{hostname} 解析到内网地址 {ip}")
    except socket.gaierror:
        raise ValueError("域名解析失败")

    # 限制超时,禁止重定向到内网(防 SSRF via 重定向)
    response = requests.get(
        url,
        timeout=5,
        allow_redirects=False,  # 手动处理重定向并再次校验
        headers={'User-Agent': 'MyApp/1.0'}
    )
    return response

方案三:网络层隔离(最根本的防御)

除了代码层防御,更根本的是通过网络架构减少 SSRF 的危害范围:

# AWS IMDSv2 强制配置(EC2 实例元数据服务 v2)
# 相比 IMDSv1,v2 需要会话令牌(PUT 请求获取),SSRF 无法直接访问
aws ec2 modify-instance-metadata-options \
  --instance-id i-xxxxx \
  --http-tokens required \          # 强制要求会话令牌
  --http-endpoint enabled

# Kubernetes:限制 Pod 的出网规则(NetworkPolicy)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-metadata-access
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.0.0/16    # 禁止访问 AWS 元数据地址
        - 10.0.0.0/8        # 禁止访问内网(按实际需求调整)
SSRF 绕过技术(了解以更好地防御)

防御时需考虑这些绕过手段:解析 IP 后校验,而不只是字符串匹配;使用 DNS 固定(DNS pinning);网络层封锁更可靠。

本章小结

CSRF 利用浏览器自动携带 Cookie 的特性,让受害者在不知情的情况下触发操作。防御三剑客:SameSite Cookie(现代浏览器默认 Lax)+ CSRF Token(深度防御)+ Origin 验证。SSRF 让服务器成为攻击者的代理,最危险的目标是云元数据接口和内网服务。防御核心:URL 白名单 + DNS 解析后的 IP 校验 + 网络层隔离(最根本)。两种攻击都体现了"信任关系被滥用"的安全思维——攻击者不是直接攻击,而是利用受信任的中间方代为执行。