1. 项目概述从一次真实的CSRF攻击说起上周我负责维护的一个电商后台系统差点出了大篓子。凌晨三点监控告警显示一个本应只有内部管理员才能访问的“批量修改商品价格”接口在短时间内被连续调用了上百次。幸运的是我们后端有二次校验机制阻止了实际的价格篡改但日志里那一串串来自不明域名的请求还是让我惊出一身冷汗。排查后发现攻击者伪造了一个精美的钓鱼页面诱骗已登录的管理员点击页面中的脚本自动向我们的关键接口发起了POST请求。这就是一次典型的跨站请求伪造攻击。事后复盘我们意识到虽然后端有Token校验但在Nginx这一层我们配置的valid_referer防线形同虚设攻击请求被轻易放行。这促使我花了几天时间把Nginx的Referer校验机制里里外外研究了个透。今天这篇避坑指南就是把我踩过的坑、验证过的方案以及那些官方文档语焉不详的细节一次性讲清楚。无论你是运维工程师、后端开发还是安全负责人只要你的服务前面挂着Nginx并且关心如何低成本、高效率地增加一道CSRF防火墙那接下来的内容就值得你仔细看看。简单说valid_referer是Nginx内置的一个指令用于检查HTTP请求头中的Referer或Origin字段值是否来自被允许的源域名。它的初衷就是为了防御CSRF这类依赖用户浏览器发起非预期请求的攻击。原理很直观一个正常的用户操作比如从你的网站www.your-site.com的表单提交数据其请求的Referer头必然是www.your-site.com。而一个从攻击者网站evil-site.com发起的伪造请求其Referer头会是evil-site.com。通过校验这个头Nginx就能在请求到达应用服务器之前将其拦截。听起来很简单对吧但为什么你的配置总感觉时灵时不灵为什么测试环境好好的一上生产就拦不住问题就出在Referer头本身的复杂性、浏览器行为的多样性以及valid_referer指令那些微妙且容易误解的匹配规则上。接下来我们就一层层剥开它的外壳。2. valid_referer 核心机制与常见误解深度解析很多人配置valid_referer失败第一步就错了——对它的工作模式和匹配逻辑存在根本性误解。我们直接看一个最常见的、也是问题最多的配置片段location /api/ { valid_referers none blocked server_names *.example.com; if ($invalid_referer) { return 403; } proxy_pass http://backend; }这段配置的意图是保护/api/下的所有接口只允许来自*.example.com、或不带Referernone、或被代理服务器标记为blocked的请求访问。看起来没问题但坑已经埋下了。2.1 “none” 与 “blocked” 的真实含义与巨坑这是误解的重灾区。none 它匹配的是请求头中完全缺失Referer字段的情况。注意是字段不存在而不是字段值为空字符串。什么时候会没有Referer主要有两种1. 用户直接在浏览器地址栏输入URL并回车2. 从书签、本地HTML文件点击链接。但是通过JavaScript发起的fetch或XMLHttpRequest请求如果未显式设置Referer头在现代浏览器默认策略下通常会发送一个包含源Origin的Referer头而不会是完全缺失。所以指望none来放过所有“合法”的Ajax请求很可能不靠谱。blocked 这个更诡异。它匹配的是Referer头存在但其值被浏览器或中间代理如某些防火墙、安全软件修改或移除导致其不是一个有效的、完整的URL的情况。例如值可能被替换为“unknown”、“about:client”或者被截断。关键在于你无法预测和依赖哪些请求会被标记为blocked。把它当作一个“通配符”或“安全阀”是极其危险的因为它会让你的安全策略出现不可预知的漏洞。避坑心得一 除非你有非常明确且可控的场景例如你知道并接受某些特定客户端或内部工具会发送无Referer请求否则不要轻易将none和blocked放入生产环境的valid_referers列表中。它们会为攻击者打开一扇后门。一个更安全的做法是初期只配置你明确信任的域名然后通过日志密切监控被403拦截的合法请求再逐步调整策略。2.2 域名匹配的“潜规则”它比你想象的更严格valid_referer的域名匹配不是简单的字符串包含。协议敏感http://example.com和https://example.com被认为是两个不同的源。如果你只配置了example.com不带协议Nginx会同时匹配http和https。但如果你配置了https://example.com那么来自http://example.com的请求就会被拒绝。在全民HTTPS的时代这似乎不是问题但如果你有HTTP到HTTPS的重定向或者在混合协议的环境下这就可能引发问题。端口敏感example.com和example.com:8080也被认为是不同的源。Nginx的匹配是基于“协议主机名端口”这个三元组的。如果你的网站有非标准端口必须显式声明。通配符*的局限性*.example.com可以匹配a.example.com、b.example.com但它不能匹配example.com根域名。同样它也不能匹配多级子域名如test.a.example.com除非你配置*.*.example.com但Nginx不支持这种写法。通配符只作用于域名标签的开头部分。正则表达式匹配的陷阱 Nginx支持在域名中使用正则表达式例如~^(www\.)?example\.com$。这很强大但也容易写错。一个常见的错误是忘记转义点号.导致它匹配任意字符。另一个陷阱是正则匹配性能开销较大在高并发场景下需谨慎使用。2.3 $invalid_referer 变量的“反向逻辑”这是另一个容易搞晕的地方。$invalid_referer变量在请求的Referer不匹配valid_referers列表中任何一项时其值为1表示无效反之匹配成功则为0或空值。所以if ($invalid_referer) { return 403; }这句话的意思是“如果来源无效则拒绝访问”。这个逻辑是反直觉的我们通常想的是“如果来源有效则放行”。但Nginx的配置逻辑就是如此你必须适应。3. 实战配置从基础到高可用方案理解了原理我们来动手配置。我会给出几个不同场景下的配置方案并解释每个参数的选择理由。3.1 基础防护配置适用于大多数Web应用假设你的主站是https://www.myshop.com还有一个管理后台子域名https://admin.myshop.com所有API接口都在/api/路径下。server { listen 443 ssl; server_name www.myshop.com; # ... SSL证书配置省略 ... # 保护所有API接口 location ~ ^/api/. { # 步骤1定义合法来源 # 只允许来自自己主站和管理后台的请求 # 使用 ~* 进行不区分大小写的正则匹配更灵活 valid_referers none blocked server_names ~*^https?://(www\.)?myshop\.com(:[0-9])?$ ~*^https?://admin\.myshop\.com(:[0-9])?$; # 步骤2校验并拦截 if ($invalid_referer) { # 记录日志以便审计 access_log /var/log/nginx/csrf_blocked.log main; # 返回403禁止访问 return 403; # 你也可以选择返回一个自定义错误页面 # return 403 /error/csrf.html; } # 步骤3通过校验代理到后端 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://backend_server; } # 其他静态资源或页面路由 location / { root /var/www/html; index index.html; } }配置解析与要点正则表达式详解~*^https?://(www\.)?myshop\.com(:[0-9])?$~* 不区分大小写的正则匹配。^https?:// 匹配以http://或https://开头。(www\.)?www.子域名可选。myshop\.com 匹配根域名注意转义点号。(:[0-9])? 端口号可选匹配如:8080。$ 字符串结束。这确保了只匹配完整的域名防止evilmyshop.com这类域名被意外匹配。为什么不用server_names 这里的server_names会匹配当前server块的server_name即www.myshop.com。但我们还需要匹配admin.myshop.com所以显式列出正则更清晰可控。日志记录 将拦截的请求记录到独立日志文件对于后续安全审计和策略调优至关重要。3.2 应对复杂场景SPA、API网关与移动端现代应用架构更复杂你的API可能同时服务于网页SPA、手机App、甚至第三方合作伙伴。场景一单页应用SPA的所有页面都来自同一个源如https://app.myshop.com但页面内的路由切换如Vue Router的history模式或API请求其Referer头始终是这个源。配置很简单参照3.1即可。但要特别注意首次页面加载和直接访问深链接时可能没有Referer触发none规则你需要根据业务决定是否放行。场景二移动端App原生iOS/Android App发起的网络请求其Referer头可能是自定义的或者根本没有。绝对不能使用none blocked来放行所有App请求这等于完全禁用Referer校验。正确做法为移动端API使用独立域名或路径 例如https://api.myshop.com/mobile/v1/。在该路径下禁用valid_referer校验转而使用更安全的认证方式如OAuth 2.0、带有时效性的签名HMAC或客户端证书。或者在App中固定设置一个合法的Referer头如https://app.myshop.com/mobile并在Nginx中配置匹配这个固定值。但这要求你能完全控制客户端代码。# 移动端API专用路径使用Token认证不依赖Referer location /mobile/v1/ { # 这里不配置valid_referer # 而是通过请求头中的Authorization Token进行校验 # 假设后端应用会校验这个Token proxy_set_header Authorization $http_authorization; proxy_pass http://backend_server; }场景三第三方回调/Webhook当你的服务需要接收来自第三方如支付平台、社交媒体的回调通知时对方的请求Referer是不可控的。解决方案为回调使用专用、高熵值的URL路径例如/webhook/payment/unique-secret-token。通过路径本身的秘密性作为第一道防线。在该路径的location块中完全禁用valid_referer。必须结合其他强验证如验证对方IP白名单、校验请求签名例如支付平台的签名算法。location ~ ^/webhook/payment/[A-Za-z0-9_-]$ { # 禁用Referer检查 # valid_referers none; # 或者直接不写valid_referers指令 # 但必须进行IP白名单校验 allow 203.0.113.1; # 支付平台的IP allow 198.51.100.0/24; deny all; # 将签名等信息传递给后端做二次校验 proxy_pass http://backend_server; }3.3 高可用与灰度发布配置直接在生产环境全量修改valid_referer规则是危险的可能误杀大量合法流量。灰度发布策略先监控后拦截 最初只记录$invalid_referer不返回403。location /api/ { valid_referers ...; set $log_me ; if ($invalid_referer) { set $log_me POTENTIAL_CSRF; } access_log /var/log/nginx/access.log main if$log_me; proxy_pass http://backend; }运行一段时间如24小时分析日志确认被标记的请求是否都是非法的。如果有合法请求被误标调整你的valid_referers列表。使用Nginx Map实现动态开关或分级处理 利用map指令可以根据条件如特定Cookie、请求头、IP段动态决定是否跳过Referer检查方便做小流量测试。# 在http块中定义map map $cookie_debug_csrf $skip_referer_check { default 0; true 1; # 当cookie debug_csrftrue时跳过检查 } server { ... location /api/ { valid_referers ...; if ($invalid_referer) { # 如果设置了调试cookie则只记录不拦截 if ($skip_referer_check) { access_log /var/log/nginx/csrf_warn.log main; break; # 继续执行proxy_pass } return 403; } proxy_pass http://backend; } }4. 为什么配置会“失效”—— 七大典型问题排查实录即使配置看起来完美在实际运行中仍可能遇到各种“失效”情况。下面是我总结的七大常见问题及其排查思路。4.1 问题一HTTPS网站配置了HTTP的Referer现象 网站全站HTTPS但Nginx配置中只写了http://example.com导致所有来自https://example.com的请求被拒。排查# 查看Nginx当前加载的配置 nginx -T # 或者直接grep相关配置 grep -r valid_referers /etc/nginx/解决 将配置改为https://example.com或直接使用example.com不写协议以同时匹配HTTP和HTTPS。4.2 问题二浏览器隐私策略与Referrer-Policy头现代浏览器越来越重视隐私。页面可以通过设置Referrer-Policy这个HTTP响应头来控制浏览器发送Referer头的策略。例如Referrer-Policy: no-referrer 完全不发送Referer头。Referrer-Policy: same-origin 仅在同源请求时发送。Referrer-Policy: strict-origin-when-cross-origin 跨域时只发送源协议主机端口不发送完整路径默认策略Chrome 85。如果你的网站页面设置了过于严格的Referrer-Policy可能导致即使是同站内的合法API请求其Referer头也被浏览器剥离或简化从而被Nginx拒绝。排查打开浏览器开发者工具F12。在“网络”(Network)标签页中找到你的API请求。查看请求头(Request Headers)确认Referer是否存在以及值是否完整。同时查看页面或API响应的响应头(Response Headers)检查是否有Referrer-Policy。解决 调整你的Referrer-Policy。对于需要严格进行CSRF防护的站点建议设置为strict-origin-when-cross-origin默认且安全或针对特定路径放宽。绝对不要为了通过Nginx校验而设置为unsafe-url始终发送完整URL这会泄露用户敏感URL参数。4.3 问题三Nginx配置作用域与继承关系错误Nginx配置有继承关系。如果你在http块或server块定义了valid_referers但在某个location块中又使用了if ($invalid_referer)这个$invalid_referer变量是基于最后生效的valid_referers指令计算的。错误示例server { valid_referers none; # 服务器级允许无Referer location /api/ { valid_referers server_names; # 位置级只允许本服务器名 if ($invalid_referer) { # 这里用的是位置级的规则 return 403; } proxy_pass http://backend; } }在这个例子中/api/的校验规则是server_namesnone在这里无效。排查 仔细检查目标location块内及其所有父级块server,http中的valid_referers指令确认最终生效的是哪一条。4.4 问题四if指令的“坑”与局限性Nginx的if指令在其上下文中存在一些众所周知的陷阱。虽然if ($invalid_referer)在简单的场景下工作良好但在复杂的location块中与其他指令如try_files,rewrite混用时可能会产生意想不到的行为因为if会创建一个隐式的嵌套位置。更稳健的写法 使用map指令将校验逻辑提前。http { map $invalid_referer $csrf_block { default 0; 1 1; # 当$invalid_referer为1时$csrf_block为1 } } server { location /api/ { valid_referers ...; if ($csrf_block) { return 403; } proxy_pass http://backend; } }或者考虑将校验逻辑放到一个单独的location块然后使用error_page或auth_request模块进行更精细的控制。4.5 问题五CDN、负载均衡器或前端代理“吃掉”了Referer如果你的架构是用户 - CDN - Nginx - 后端那么Nginx看到的Referer头可能是CDN添加或修改过的。有些CDN默认会过滤或重写Referer头。排查在Nginx的访问日志中记录原始的$http_referer。log_format detailed $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent; access_log /var/log/nginx/detailed.log detailed;同时在你的应用后端也记录接收到的Referer头。对比两者。如果Nginx看到的和CDN接收到的不同问题就在CDN。解决 查阅你的CDN服务商文档确认其Referer头处理策略。通常可以在CDN控制台配置“保留客户端请求头”或“透传Referer”。如果CDN修改了Referer你可能需要在Nginx中校验CDN添加的特定头如X-Forwarded-Host,X-Real-Referer等但这需要CDN支持且配置对应。4.6 问题六正则表达式编写错误一个错误的.转义或$结束符就可能导致匹配失败。排查 使用在线正则表达式测试工具如 regex101.com反复测试你的模式串确保它能正确匹配你期望的合法来源并排除非法来源。示例 要匹配https://sub.example.com和https://example.com但不匹配https://evilexample.com。错误~*example\.com会匹配到evilexample.com正确~*^https?://(.*\.)?example\.com(:[0-9])?/?$确保example.com是域名的结尾部分4.7 问题七缓存与配置未重载你修改了Nginx配置文件但没有重载或重启Nginx服务。排查与解决# 1. 测试配置文件语法 nginx -t # 2. 如果语法正确重载配置平滑重启不影响在线请求 nginx -s reload # 或使用systemd sudo systemctl reload nginx5. 超越valid_referer构建纵深防御体系必须清醒认识到valid_referer不能作为防御CSRF的唯一手段。它是一道有效的、低成本的前置防线但存在固有缺陷依赖浏览器行为Referer头可以被用户浏览器设置、安全软件或浏览器扩展禁用。并非所有请求都有Referer 如前所述从书签、地址栏、本地文件发起的请求可能没有。存在被篡改的风险 虽然难度较大但在某些中间人攻击场景下Referer头可能被伪造。因此一个健壮的CSRF防御体系应该是多层次的第一层Nginx valid_referer本文重点作用 拦截绝大多数来自不明外站的、明显的自动化攻击脚本。优点 部署简单性能开销极低对后端应用零侵入。定位 外围粗粒度过滤。第二层同源策略与CORS浏览器层面正确配置CORS跨源资源共享头确保只有受信任的源才能通过浏览器发起跨域请求。这能阻止恶意网站通过用户浏览器发起的简单攻击。第三层CSRF Tokens应用层核心防御为每个用户会话生成一个随机的、不可预测的Token嵌入到表单或作为请求头如X-CSRF-TOKEN。后端在处理状态变更请求POST, PUT, DELETE时必须校验此Token。这是目前最主流、最可靠的防御方式。Token应具有时效性并与用户会话绑定。第四层SameSite CookiesCookie层面为你的认证Cookie设置SameSiteStrict或SameSiteLax属性。这可以阻止浏览器在跨站请求中自动携带Cookie从而从根本上切断许多CSRF攻击的凭据传递路径。Strict最安全但可能影响用户体验例如从邮件链接点击登录Lax是一个很好的平衡。第五层关键操作二次验证对于修改密码、转账、删除数据等极高风险操作强制要求用户进行二次验证如输入密码、短信验证码、生物识别等。将valid_referer作为这个纵深防御体系的第一道门槛它能以极小的成本过滤掉大量“噪音”攻击让后几层更专注于防御更复杂、更隐蔽的攻击手段。在实际部署中我建议的流程是先严格配置valid_referer并观察日志确保无误杀然后在此基础上确保应用层的CSRF Token和Cookie的SameSite属性已正确实施。这样即使某一层防御因配置问题暂时失效其他层仍然能提供保护。最后安全是一个持续的过程。定期审查Nginx的拦截日志关注浏览器安全策略的更新如Referrer-Policy的演变并根据业务变化调整你的valid_referers白名单才能让这道防线持续有效地运转。
Nginx valid_referer配置实战:从原理到高可用CSRF防御方案
1. 项目概述从一次真实的CSRF攻击说起上周我负责维护的一个电商后台系统差点出了大篓子。凌晨三点监控告警显示一个本应只有内部管理员才能访问的“批量修改商品价格”接口在短时间内被连续调用了上百次。幸运的是我们后端有二次校验机制阻止了实际的价格篡改但日志里那一串串来自不明域名的请求还是让我惊出一身冷汗。排查后发现攻击者伪造了一个精美的钓鱼页面诱骗已登录的管理员点击页面中的脚本自动向我们的关键接口发起了POST请求。这就是一次典型的跨站请求伪造攻击。事后复盘我们意识到虽然后端有Token校验但在Nginx这一层我们配置的valid_referer防线形同虚设攻击请求被轻易放行。这促使我花了几天时间把Nginx的Referer校验机制里里外外研究了个透。今天这篇避坑指南就是把我踩过的坑、验证过的方案以及那些官方文档语焉不详的细节一次性讲清楚。无论你是运维工程师、后端开发还是安全负责人只要你的服务前面挂着Nginx并且关心如何低成本、高效率地增加一道CSRF防火墙那接下来的内容就值得你仔细看看。简单说valid_referer是Nginx内置的一个指令用于检查HTTP请求头中的Referer或Origin字段值是否来自被允许的源域名。它的初衷就是为了防御CSRF这类依赖用户浏览器发起非预期请求的攻击。原理很直观一个正常的用户操作比如从你的网站www.your-site.com的表单提交数据其请求的Referer头必然是www.your-site.com。而一个从攻击者网站evil-site.com发起的伪造请求其Referer头会是evil-site.com。通过校验这个头Nginx就能在请求到达应用服务器之前将其拦截。听起来很简单对吧但为什么你的配置总感觉时灵时不灵为什么测试环境好好的一上生产就拦不住问题就出在Referer头本身的复杂性、浏览器行为的多样性以及valid_referer指令那些微妙且容易误解的匹配规则上。接下来我们就一层层剥开它的外壳。2. valid_referer 核心机制与常见误解深度解析很多人配置valid_referer失败第一步就错了——对它的工作模式和匹配逻辑存在根本性误解。我们直接看一个最常见的、也是问题最多的配置片段location /api/ { valid_referers none blocked server_names *.example.com; if ($invalid_referer) { return 403; } proxy_pass http://backend; }这段配置的意图是保护/api/下的所有接口只允许来自*.example.com、或不带Referernone、或被代理服务器标记为blocked的请求访问。看起来没问题但坑已经埋下了。2.1 “none” 与 “blocked” 的真实含义与巨坑这是误解的重灾区。none 它匹配的是请求头中完全缺失Referer字段的情况。注意是字段不存在而不是字段值为空字符串。什么时候会没有Referer主要有两种1. 用户直接在浏览器地址栏输入URL并回车2. 从书签、本地HTML文件点击链接。但是通过JavaScript发起的fetch或XMLHttpRequest请求如果未显式设置Referer头在现代浏览器默认策略下通常会发送一个包含源Origin的Referer头而不会是完全缺失。所以指望none来放过所有“合法”的Ajax请求很可能不靠谱。blocked 这个更诡异。它匹配的是Referer头存在但其值被浏览器或中间代理如某些防火墙、安全软件修改或移除导致其不是一个有效的、完整的URL的情况。例如值可能被替换为“unknown”、“about:client”或者被截断。关键在于你无法预测和依赖哪些请求会被标记为blocked。把它当作一个“通配符”或“安全阀”是极其危险的因为它会让你的安全策略出现不可预知的漏洞。避坑心得一 除非你有非常明确且可控的场景例如你知道并接受某些特定客户端或内部工具会发送无Referer请求否则不要轻易将none和blocked放入生产环境的valid_referers列表中。它们会为攻击者打开一扇后门。一个更安全的做法是初期只配置你明确信任的域名然后通过日志密切监控被403拦截的合法请求再逐步调整策略。2.2 域名匹配的“潜规则”它比你想象的更严格valid_referer的域名匹配不是简单的字符串包含。协议敏感http://example.com和https://example.com被认为是两个不同的源。如果你只配置了example.com不带协议Nginx会同时匹配http和https。但如果你配置了https://example.com那么来自http://example.com的请求就会被拒绝。在全民HTTPS的时代这似乎不是问题但如果你有HTTP到HTTPS的重定向或者在混合协议的环境下这就可能引发问题。端口敏感example.com和example.com:8080也被认为是不同的源。Nginx的匹配是基于“协议主机名端口”这个三元组的。如果你的网站有非标准端口必须显式声明。通配符*的局限性*.example.com可以匹配a.example.com、b.example.com但它不能匹配example.com根域名。同样它也不能匹配多级子域名如test.a.example.com除非你配置*.*.example.com但Nginx不支持这种写法。通配符只作用于域名标签的开头部分。正则表达式匹配的陷阱 Nginx支持在域名中使用正则表达式例如~^(www\.)?example\.com$。这很强大但也容易写错。一个常见的错误是忘记转义点号.导致它匹配任意字符。另一个陷阱是正则匹配性能开销较大在高并发场景下需谨慎使用。2.3 $invalid_referer 变量的“反向逻辑”这是另一个容易搞晕的地方。$invalid_referer变量在请求的Referer不匹配valid_referers列表中任何一项时其值为1表示无效反之匹配成功则为0或空值。所以if ($invalid_referer) { return 403; }这句话的意思是“如果来源无效则拒绝访问”。这个逻辑是反直觉的我们通常想的是“如果来源有效则放行”。但Nginx的配置逻辑就是如此你必须适应。3. 实战配置从基础到高可用方案理解了原理我们来动手配置。我会给出几个不同场景下的配置方案并解释每个参数的选择理由。3.1 基础防护配置适用于大多数Web应用假设你的主站是https://www.myshop.com还有一个管理后台子域名https://admin.myshop.com所有API接口都在/api/路径下。server { listen 443 ssl; server_name www.myshop.com; # ... SSL证书配置省略 ... # 保护所有API接口 location ~ ^/api/. { # 步骤1定义合法来源 # 只允许来自自己主站和管理后台的请求 # 使用 ~* 进行不区分大小写的正则匹配更灵活 valid_referers none blocked server_names ~*^https?://(www\.)?myshop\.com(:[0-9])?$ ~*^https?://admin\.myshop\.com(:[0-9])?$; # 步骤2校验并拦截 if ($invalid_referer) { # 记录日志以便审计 access_log /var/log/nginx/csrf_blocked.log main; # 返回403禁止访问 return 403; # 你也可以选择返回一个自定义错误页面 # return 403 /error/csrf.html; } # 步骤3通过校验代理到后端 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://backend_server; } # 其他静态资源或页面路由 location / { root /var/www/html; index index.html; } }配置解析与要点正则表达式详解~*^https?://(www\.)?myshop\.com(:[0-9])?$~* 不区分大小写的正则匹配。^https?:// 匹配以http://或https://开头。(www\.)?www.子域名可选。myshop\.com 匹配根域名注意转义点号。(:[0-9])? 端口号可选匹配如:8080。$ 字符串结束。这确保了只匹配完整的域名防止evilmyshop.com这类域名被意外匹配。为什么不用server_names 这里的server_names会匹配当前server块的server_name即www.myshop.com。但我们还需要匹配admin.myshop.com所以显式列出正则更清晰可控。日志记录 将拦截的请求记录到独立日志文件对于后续安全审计和策略调优至关重要。3.2 应对复杂场景SPA、API网关与移动端现代应用架构更复杂你的API可能同时服务于网页SPA、手机App、甚至第三方合作伙伴。场景一单页应用SPA的所有页面都来自同一个源如https://app.myshop.com但页面内的路由切换如Vue Router的history模式或API请求其Referer头始终是这个源。配置很简单参照3.1即可。但要特别注意首次页面加载和直接访问深链接时可能没有Referer触发none规则你需要根据业务决定是否放行。场景二移动端App原生iOS/Android App发起的网络请求其Referer头可能是自定义的或者根本没有。绝对不能使用none blocked来放行所有App请求这等于完全禁用Referer校验。正确做法为移动端API使用独立域名或路径 例如https://api.myshop.com/mobile/v1/。在该路径下禁用valid_referer校验转而使用更安全的认证方式如OAuth 2.0、带有时效性的签名HMAC或客户端证书。或者在App中固定设置一个合法的Referer头如https://app.myshop.com/mobile并在Nginx中配置匹配这个固定值。但这要求你能完全控制客户端代码。# 移动端API专用路径使用Token认证不依赖Referer location /mobile/v1/ { # 这里不配置valid_referer # 而是通过请求头中的Authorization Token进行校验 # 假设后端应用会校验这个Token proxy_set_header Authorization $http_authorization; proxy_pass http://backend_server; }场景三第三方回调/Webhook当你的服务需要接收来自第三方如支付平台、社交媒体的回调通知时对方的请求Referer是不可控的。解决方案为回调使用专用、高熵值的URL路径例如/webhook/payment/unique-secret-token。通过路径本身的秘密性作为第一道防线。在该路径的location块中完全禁用valid_referer。必须结合其他强验证如验证对方IP白名单、校验请求签名例如支付平台的签名算法。location ~ ^/webhook/payment/[A-Za-z0-9_-]$ { # 禁用Referer检查 # valid_referers none; # 或者直接不写valid_referers指令 # 但必须进行IP白名单校验 allow 203.0.113.1; # 支付平台的IP allow 198.51.100.0/24; deny all; # 将签名等信息传递给后端做二次校验 proxy_pass http://backend_server; }3.3 高可用与灰度发布配置直接在生产环境全量修改valid_referer规则是危险的可能误杀大量合法流量。灰度发布策略先监控后拦截 最初只记录$invalid_referer不返回403。location /api/ { valid_referers ...; set $log_me ; if ($invalid_referer) { set $log_me POTENTIAL_CSRF; } access_log /var/log/nginx/access.log main if$log_me; proxy_pass http://backend; }运行一段时间如24小时分析日志确认被标记的请求是否都是非法的。如果有合法请求被误标调整你的valid_referers列表。使用Nginx Map实现动态开关或分级处理 利用map指令可以根据条件如特定Cookie、请求头、IP段动态决定是否跳过Referer检查方便做小流量测试。# 在http块中定义map map $cookie_debug_csrf $skip_referer_check { default 0; true 1; # 当cookie debug_csrftrue时跳过检查 } server { ... location /api/ { valid_referers ...; if ($invalid_referer) { # 如果设置了调试cookie则只记录不拦截 if ($skip_referer_check) { access_log /var/log/nginx/csrf_warn.log main; break; # 继续执行proxy_pass } return 403; } proxy_pass http://backend; } }4. 为什么配置会“失效”—— 七大典型问题排查实录即使配置看起来完美在实际运行中仍可能遇到各种“失效”情况。下面是我总结的七大常见问题及其排查思路。4.1 问题一HTTPS网站配置了HTTP的Referer现象 网站全站HTTPS但Nginx配置中只写了http://example.com导致所有来自https://example.com的请求被拒。排查# 查看Nginx当前加载的配置 nginx -T # 或者直接grep相关配置 grep -r valid_referers /etc/nginx/解决 将配置改为https://example.com或直接使用example.com不写协议以同时匹配HTTP和HTTPS。4.2 问题二浏览器隐私策略与Referrer-Policy头现代浏览器越来越重视隐私。页面可以通过设置Referrer-Policy这个HTTP响应头来控制浏览器发送Referer头的策略。例如Referrer-Policy: no-referrer 完全不发送Referer头。Referrer-Policy: same-origin 仅在同源请求时发送。Referrer-Policy: strict-origin-when-cross-origin 跨域时只发送源协议主机端口不发送完整路径默认策略Chrome 85。如果你的网站页面设置了过于严格的Referrer-Policy可能导致即使是同站内的合法API请求其Referer头也被浏览器剥离或简化从而被Nginx拒绝。排查打开浏览器开发者工具F12。在“网络”(Network)标签页中找到你的API请求。查看请求头(Request Headers)确认Referer是否存在以及值是否完整。同时查看页面或API响应的响应头(Response Headers)检查是否有Referrer-Policy。解决 调整你的Referrer-Policy。对于需要严格进行CSRF防护的站点建议设置为strict-origin-when-cross-origin默认且安全或针对特定路径放宽。绝对不要为了通过Nginx校验而设置为unsafe-url始终发送完整URL这会泄露用户敏感URL参数。4.3 问题三Nginx配置作用域与继承关系错误Nginx配置有继承关系。如果你在http块或server块定义了valid_referers但在某个location块中又使用了if ($invalid_referer)这个$invalid_referer变量是基于最后生效的valid_referers指令计算的。错误示例server { valid_referers none; # 服务器级允许无Referer location /api/ { valid_referers server_names; # 位置级只允许本服务器名 if ($invalid_referer) { # 这里用的是位置级的规则 return 403; } proxy_pass http://backend; } }在这个例子中/api/的校验规则是server_namesnone在这里无效。排查 仔细检查目标location块内及其所有父级块server,http中的valid_referers指令确认最终生效的是哪一条。4.4 问题四if指令的“坑”与局限性Nginx的if指令在其上下文中存在一些众所周知的陷阱。虽然if ($invalid_referer)在简单的场景下工作良好但在复杂的location块中与其他指令如try_files,rewrite混用时可能会产生意想不到的行为因为if会创建一个隐式的嵌套位置。更稳健的写法 使用map指令将校验逻辑提前。http { map $invalid_referer $csrf_block { default 0; 1 1; # 当$invalid_referer为1时$csrf_block为1 } } server { location /api/ { valid_referers ...; if ($csrf_block) { return 403; } proxy_pass http://backend; } }或者考虑将校验逻辑放到一个单独的location块然后使用error_page或auth_request模块进行更精细的控制。4.5 问题五CDN、负载均衡器或前端代理“吃掉”了Referer如果你的架构是用户 - CDN - Nginx - 后端那么Nginx看到的Referer头可能是CDN添加或修改过的。有些CDN默认会过滤或重写Referer头。排查在Nginx的访问日志中记录原始的$http_referer。log_format detailed $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent; access_log /var/log/nginx/detailed.log detailed;同时在你的应用后端也记录接收到的Referer头。对比两者。如果Nginx看到的和CDN接收到的不同问题就在CDN。解决 查阅你的CDN服务商文档确认其Referer头处理策略。通常可以在CDN控制台配置“保留客户端请求头”或“透传Referer”。如果CDN修改了Referer你可能需要在Nginx中校验CDN添加的特定头如X-Forwarded-Host,X-Real-Referer等但这需要CDN支持且配置对应。4.6 问题六正则表达式编写错误一个错误的.转义或$结束符就可能导致匹配失败。排查 使用在线正则表达式测试工具如 regex101.com反复测试你的模式串确保它能正确匹配你期望的合法来源并排除非法来源。示例 要匹配https://sub.example.com和https://example.com但不匹配https://evilexample.com。错误~*example\.com会匹配到evilexample.com正确~*^https?://(.*\.)?example\.com(:[0-9])?/?$确保example.com是域名的结尾部分4.7 问题七缓存与配置未重载你修改了Nginx配置文件但没有重载或重启Nginx服务。排查与解决# 1. 测试配置文件语法 nginx -t # 2. 如果语法正确重载配置平滑重启不影响在线请求 nginx -s reload # 或使用systemd sudo systemctl reload nginx5. 超越valid_referer构建纵深防御体系必须清醒认识到valid_referer不能作为防御CSRF的唯一手段。它是一道有效的、低成本的前置防线但存在固有缺陷依赖浏览器行为Referer头可以被用户浏览器设置、安全软件或浏览器扩展禁用。并非所有请求都有Referer 如前所述从书签、地址栏、本地文件发起的请求可能没有。存在被篡改的风险 虽然难度较大但在某些中间人攻击场景下Referer头可能被伪造。因此一个健壮的CSRF防御体系应该是多层次的第一层Nginx valid_referer本文重点作用 拦截绝大多数来自不明外站的、明显的自动化攻击脚本。优点 部署简单性能开销极低对后端应用零侵入。定位 外围粗粒度过滤。第二层同源策略与CORS浏览器层面正确配置CORS跨源资源共享头确保只有受信任的源才能通过浏览器发起跨域请求。这能阻止恶意网站通过用户浏览器发起的简单攻击。第三层CSRF Tokens应用层核心防御为每个用户会话生成一个随机的、不可预测的Token嵌入到表单或作为请求头如X-CSRF-TOKEN。后端在处理状态变更请求POST, PUT, DELETE时必须校验此Token。这是目前最主流、最可靠的防御方式。Token应具有时效性并与用户会话绑定。第四层SameSite CookiesCookie层面为你的认证Cookie设置SameSiteStrict或SameSiteLax属性。这可以阻止浏览器在跨站请求中自动携带Cookie从而从根本上切断许多CSRF攻击的凭据传递路径。Strict最安全但可能影响用户体验例如从邮件链接点击登录Lax是一个很好的平衡。第五层关键操作二次验证对于修改密码、转账、删除数据等极高风险操作强制要求用户进行二次验证如输入密码、短信验证码、生物识别等。将valid_referer作为这个纵深防御体系的第一道门槛它能以极小的成本过滤掉大量“噪音”攻击让后几层更专注于防御更复杂、更隐蔽的攻击手段。在实际部署中我建议的流程是先严格配置valid_referer并观察日志确保无误杀然后在此基础上确保应用层的CSRF Token和Cookie的SameSite属性已正确实施。这样即使某一层防御因配置问题暂时失效其他层仍然能提供保护。最后安全是一个持续的过程。定期审查Nginx的拦截日志关注浏览器安全策略的更新如Referrer-Policy的演变并根据业务变化调整你的valid_referers白名单才能让这道防线持续有效地运转。