跳转到帖子

授权测试中伪造 X-Forwarded-For 能访问管理接口,如何定位信任边界并验证修复?

精选回复

发布于

背景:同一个接口,为什么换个请求头就能访问?

问题场景:在授权测试环境中,业务链路为“客户端 → 负载均衡 LB → Nginx → 应用”。管理接口按来源 IP 限制访问,普通测试出口请求返回 403,但添加 X-Forwarded-For 后却返回 200。应该检查 Nginx、应用代码,还是负载均衡配置?

这类问题的关键不是“请求头能不能伪造”,而是:谁有权声明客户端地址,以及这份声明在哪一层参与了授权。下面以只读接口 /admin/health 为例,给出定位和复测方法。所有请求均应限定在书面授权的环境、域名和路径内。

问题定位:先确认是谁放行了请求

不要仅凭 200 判断权限绕过成立。登录页面、统一错误页面也可能返回 200。需要同时确认响应内容确实来自受限接口,以及请求是否到达应用。

  • 记录正常请求与修改请求的时间、状态码、响应内容特征和请求 ID。
  • 检查 Nginx 是否配置 allow/deny,以及应用是否另外解析了 X-Forwarded-For 或 X-Real-IP。
  • 检查 real_ip_header、set_real_ip_from、real_ip_recursive 的实际生效值。
  • 确认源站端口是否只允许 LB 访问,避免把“经过代理”和“直连源站”混为一谈。

先查看实际加载的配置,而不是只检查某个配置文件。输出可能含内部域名、证书路径等信息,不要直接贴到公开帖子。

sudo nginx -T 2>&1 | less

# 快速定位相关配置,最终仍需结合所在的 http/server/location 上下文判断
sudo nginx -T 2>&1 | grep -nE \
'real_ip_header|set_real_ip_from|real_ip_recursive|allow |deny |proxy_set_header|access_log'

# 确认应用与代理的监听地址
sudo ss -lntp

技术分析:两个常见的错误信任点

第一类问题是应用直接取 X-Forwarded-For 的第一个地址作为客户端 IP,并据此执行管理接口白名单。若入口没有覆盖或规范化这个头,外部输入就变成了授权依据。

第二类问题是 Nginx 过度信任代理来源,例如配置 set_real_ip_from 0.0.0.0/0。启用 real_ip 模块后,可信来源提供的地址会用于改写 $remote_addr;此时 allow/deny 可能依据被改写的地址执行。把所有来源都列为可信,相当于允许任意连接方参与声明自己的身份。

还有一种容易遗漏的情况:Nginx 已正确恢复客户端地址,但应用仍自行解析原始 X-Forwarded-For,导致两层使用不同的 IP 判断权限。

来源 IP 不是从请求头里“读取出来”就可信了。可信客户端地址应由受控代理链计算,并通过明确约定传给下一层。

操作步骤:用最小证据区分故障位置

先在测试环境增加结构化访问日志。以下 log_format 放在 http 上下文,access_log 放入对应 server。$realip_remote_addr 用于记录地址改写前的连接对端,$remote_addr 则是当前生效地址;该变量依赖 real_ip 模块支持。

log_format ip_boundary escape=json
  '{"time":"$time_iso8601",'
  '"rid":"$request_id",'
  '"peer":"$realip_remote_addr",'
  '"client":"$remote_addr",'
  '"xff":"$http_x_forwarded_for",'
  '"path":"$uri",'
  '"status":"$status",'
  '"upstream_status":"$upstream_status"}';

# 放入目标 server
access_log /var/log/nginx/ip_boundary.log ip_boundary;

从一个不在管理白名单内、但被授权用于验证的出口发起以下请求。示例地址属于文档保留地址,必须替换为测试环境实际配置的白名单地址;不要为了复测将测试出口加入白名单。

# 基线:预期被管理接口的来源限制拒绝
curl --max-time 10 -sS -D - \
  

# 仅改变一个变量:客户端提交的来源声明
curl --max-time 10 -sS -D - \
  -H 'X-Forwarded-For: 198.51.100.23' \
  

# 单独检查应用是否错误信任另一个头
curl --max-time 10 -sS -D - \
  -H 'X-Real-IP: 198.51.100.23' \
  

# 在对应时间窗口核对日志
sudo tail -n 50 /var/log/nginx/ip_boundary.log
  • 若修改头后 client 变成白名单地址,优先检查 Nginx 的代理信任配置,以及 LB 实际如何处理外部传入的头。
  • 若 client 不变,但应用从拒绝变成允许,优先检查应用自己的客户端 IP 提取逻辑。
  • 若 status 为 403 且 upstream_status 为“-”,通常说明请求没有进入上游;仍应结合 location 和错误处理配置确认。
  • 若 upstream_status 有值,则检查应用日志,确认应用实际看到的连接来源和授权依据。

关键配置:收紧信任来源,并统一传递地址

下面适用于一个明确的简化拓扑:只有 LB 能连接该 Nginx,LB 的对端地址是 192.0.2.10,LB 会可靠记录真实客户端地址,应用只接受本机 Nginx 的连接。配置片段应合并到现有配置,不能脱离实际网络直接照搬。

# 放入目标 server 或合适的 http 上下文
# 这里只信任实际连接 Nginx 的 LB 地址
set_real_ip_from 192.0.2.10/32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

# 以下代理头统一放在 server 上下文
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
proxy_set_header X-Real-IP $remote_addr;

# 对只经此 Nginx 访问的应用,传递单一、已规范化的客户端地址
proxy_set_header X-Forwarded-For $remote_addr;

location /admin/ {
    allow 198.51.100.0/24;
    deny all;
    proxy_pass 
}

location / {
    proxy_pass 
}

这里有三个前提需要核实。第一,LB 必须覆盖外部声明,或者以明确、正确的规则追加实际客户端地址;多级代理场景要逐跳确认,而不是简单信任整个内网网段。第二,应用必须只信任受控代理传来的规范化地址,不能继续随意选取其他来源头。第三,如果某个 location 自己定义了 proxy_set_header,需要检查继承行为,避免上述头配置在那里失效。

如果后端部署在独立主机或容器网络中,应将示例中的回环地址替换为实际服务地址,并通过安全组或防火墙限制只有 Nginx 可以连接。单靠“后端地址没有公开”不构成访问控制。

# 变更前先校验,校验通过后再按变更流程重载
sudo nginx -t && sudo systemctl reload nginx

风险与修复验收

  • 修复后,从非白名单出口分别执行基线、伪造 X-Forwarded-For、伪造 X-Real-IP 请求,结果应保持拒绝,日志中的有效客户端地址不应被这些声明改变。
  • 从合法管理出口复测,确认业务没有被误封。若还有登录认证,应使用测试账号验证完整授权链,而不是只看网络白名单。
  • 检查实际存在的管理路由及其匹配关系。示例中的 /admin/ 不自动涵盖其他前缀,也不能代替对 /admin 路由行为的检查。
  • 在明确授权下确认源站和应用端口不能绕过入口直接访问。预期应由网络策略阻断非预期来源。
  • 保留变更前后配置、请求 ID、日志和响应差异作为证据;原始来源头属于不可信输入,日志展示时仍需转义并控制访问权限。

管理接口不能只靠 IP 白名单保护。应同时保留身份认证、角色授权和敏感操作审计;必要时通过专用管理入口或双向 TLS 增强身份约束。

总结

这类问题的排查顺序是:先确认受限功能是否真的可访问,再沿着“连接对端 → Nginx 生效地址 → 转发头 → 应用授权依据”逐层核对。修复目标不是删除某个请求头,而是确保外部输入无法越过代理信任边界,并让每一层都使用同一份经过验证的客户端地址。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

这个现象通常不是“伪造 IP”本身的问题,而是应用把客户端可控的 X-Forwarded-For 当成了鉴权依据。建议先把请求链路画清楚:客户端 → CDN/WAF → LB/Ingress → 应用,确认究竟由哪一层生成、覆盖和解析该头。

可以先做一组差异测试,并同时查看每一层访问日志:

curl -i 
curl -i -H 'X-Forwarded-For: 127.0.0.1' 
curl -i -H 'X-Forwarded-For: 10.0.0.8' 
curl -i -H 'X-Forwarded-For: 203.0.113.10, 127.0.0.1' 
curl -i -H 'Forwarded: for=127.0.0.1' 

日志里至少同时记录连接源地址、原始转发头和框架解析后的客户端地址,例如 Nginx 可临时使用:

log_format ip_debug '$remote_addr '
                    'xff="$http_x_forwarded_for" '
                    'realip="$realip_remote_addr" '
                    'request="$request" status=$status';

重点确认以下几件事:

  • 公网入口是否无条件覆盖客户端传入的 X-Forwarded-For,还是直接透传/追加。
  • 应用是否能绕过代理直接访问;如果源站端口对测试网络或公网开放,攻击者可自行构造完整转发链。
  • 框架的“信任代理”是否配置成了全信任,例如 Express 的 trust proxy = true,或 Nginx 的 set_real_ip_from 0.0.0.0/0。
  • 管理接口是否仅依据“内网 IP/本机 IP”放行。IP 最多适合作为附加限制,不应替代登录、会话或服务身份认证。

Nginx 的修复思路是只信任实际反向代理网段,并在入口清洗用户传入的头。后端 Nginx 示例:

# 只填写实际 LB/Ingress 的地址段,不能写 0.0.0.0/0
set_real_ip_from 10.20.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP       $remote_addr;
    proxy_set_header Host            $host;
    proxy_pass 
}

如果这是最外层、直接接收不可信客户端流量的代理,建议不要基于客户端已有的 XFF 继续追加,而是覆盖:

proxy_set_header X-Forwarded-For $remote_addr;

Express 等框架也应显式限定受信任代理,而不是按任意头取第一个地址:

app.set('trust proxy', ['loopback', '10.20.0.0/16']);

app.get('/admin', requireAuthenticatedAdmin, (req, res) => {
  // req.ip 只能用于审计或附加访问控制
  res.sendStatus(204);
});

解析时应从连接源地址开始,按转发链从右向左剥离“已知可信代理”,第一个不可信地址才是客户端地址;直接取 XFF 最左项或用字符串前缀判断内网段都容易出问题。IP 网段判断应使用标准 CIDR 库,避免类似 startsWith("10.") 的实现。

修复后的验证不要只重放单个 PoC,建议覆盖以下矩阵:

  • 外部未认证请求,无 XFF:拒绝。
  • 外部未认证请求,伪造单个或多个 XFF/Forwarded:仍拒绝。
  • 通过正常代理访问,日志中的客户端 IP 正确。
  • 直接访问源站 IP/端口:网络层不可达,或同样要求管理身份认证。
  • 合法管理员从允许网络访问:认证和授权通过。
  • 普通已登录用户即使位于允许网段:仍因角色不足被拒绝。

最后建议把管理接口的根本控制落在认证与角色授权上,再叠加源站安全组、mTLS/VPN 或管理网段限制。这样即使后续代理链调整导致真实 IP 解析异常,也不会直接变成管理权限绕过。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。