Cloudflare和nginx后所有用户同IP:限流修复记
原文:https://dev.to/mrviduus/behind-cloudflare-and-nginx-all-my-users-had-the-same-ip-3hae(作者 @mrviduus)
我的管理后台登录页允许每分钟输错 5 次密码,超过这个次数就应该把访问者封掉。
我试了 9 次,从没被封过。
我又检查了应用里其他几处限流规则,也全都没生效。而在修复它的过程中,我发现了第二个问题:生产环境里,我的应用把所有用户都看成了同一个 IP。之前一直没人察觉,是因为限流器根本没在工作。假如我只修好第一个 bug,限流器就会带着一个全互联网共用的计数器开始运转。
限流器为什么毫无动作
在 ASP.NET Core 中,每个请求都会依次穿过一串中间件,先后顺序很重要。
其中一步是路由。它负责判断“这个请求是发给登录接口的”。限流器需要这个答案,因为每个接口有各自的限额。
在我的应用里,限流器被放在了路由之前。于是它过早地问出“这是哪个接口?”,得不到答案,就把请求放行了。全程没有报错,日志里也一片干净。
修复只需要挪一行:
app.UseRouting(); app.UseRateLimiter(); // 必须放在路由之后
(如果你从来没有自己调用过 UseRouting(),ASP.NET 会在链条开头自动替你加上,所以踩不到这个 bug。我是自己调用的,而且放在链条靠后的位置,结果限流器排到了它前面。)
这次修完,限额开始生效,紧接着又冒出三个问题。它们一直都在,只是在限流器形同虚设的时候伤不到任何人。
所有用户都是同一个 IP
我的限额按 IP 地址统计请求数。但我的应用躲在 Cloudflare 和 nginx 后面,从不与用户直接通信,用户的 IP 存放在 X-Forwarded-For 请求头里。
nginx 是这样拼出这个头的:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
$proxy_add_x_forwarded_for 会保留 nginx 收到的原头,再追加上与 nginx 建立连接的那一方的地址。在我的部署里,这个“另一方”是跑在同一台机器上的 cloudflared。于是每个请求到达应用时都是这个样子(这里的用户 IP 只是示例):
X-Forwarded-For: 203.0.113.42, 127.0.0.1
真实用户 cloudflared,由 nginx 追加
修复前,我的应用取的是:127.0.0.1
修复后,我的应用取的是:203.0.113.42
默认情况下,ASP.NET 的 Forwarded Headers 中间件只读取其中一项——最后一项。而最后一项永远是 127.0.0.1。也就是说,我那条“每个 IP 限 3 次游客注册”的规则,实际效果会变成全站访客加起来共享 3 次。
同一个位置还埋着第二个问题:我的配置信任来自任何来源的这个头。生产环境里第一个问题把它掩盖住了,可一旦去掉 nginx 这一跳,客户端就可以在头里随手写一个 IP,领到一个崭新的计数器。我在测试环境里验证过,确实可行。
两个问题的修复合在一起是这样的(略有精简):
builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
o.ForwardLimit = null; // 读取整个列表,而不只是最后一条
// 只信任我自己的代理跳数:环回地址与私有网段
o.KnownIPNetworks.Clear();
o.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("127.0.0.0/8"));
o.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("::1/128"));
o.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8"));
o.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("172.16.0.0/12"));
o.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("192.168.0.0/16"));
});
现在中间件会从列表末尾往前读,跳过它信任的地址(也就是我自己的代理),在遇到第一个不信任的地址时停下——那才是真实用户。如果客户端把伪造的 IP 塞进头里,它会落在更靠左的位置,中间件根本走不到那里。
十次错误密码就能锁死所有人
我的登录限额根本不是按 IP 计数的,而是整个站点共用一个计数器:每分钟 10 次尝试。
限流器不工作时,这无所谓。限流器一旦生效,任何人只要输错 10 次密码,就能把全站用户的登录一起关掉;密码重置也会跟着遭殃,因为它用的是同一个限额。我把它改成了每个 IP 一个计数器。攻击者仍然有 10 次机会,其他所有人也依然能正常登录。
测试全绿恰恰是因为这个 bug
限额开始生效后,我的集成测试开始失败。这些测试会在短时间内发出大量请求,以前能通过,只是因为从来没有请求被拦截过。我把各项限额改成了可配置,这样 CI 可以使用更大的数值。
我还加了两条专门检查限流器本身、而不是业务接口的测试:
- 比限额多发一个请求,预期返回
429 Too Many Requests - 让两个不同用户经过同一个代理发请求,预期得到两个独立的计数器
在此之前,我从没见过我的限流器返回 429。现在回想,那才是真正的警报,而我错过了它。
检查你自己的应用
发一批超过限额的请求——当然,只能对你自己的应用这么做:
for i in $(seq 1 11); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://your-app/login
done
你应该在末尾看到 429。如果看到的只有 200,说明你的限额没有生效。接着每次换一个伪造的 X-Forwarded-For 头再试一轮。如果 429 消失了,就说明任何人都能绕过你的限额。
你检查过自己的限流器在生产环境里是否真的返回 429 吗?我很好奇,漏掉这一步的是不是只有我一个。
本文原文最初发布于 [vasyl.blog](https://vasyl.blog/2026/10/04/behind-cloudflare-and-nginx-all-my-users-had-the-same-ip/)。
原文:https://dev.to/mrviduus/behind-cloudflare-and-nginx-all-my-users-had-the-same-ip-3hae(作者 @mrviduus)