304响应默认不携带add_header定义的安全头,必须为每个add_header指令显式添加always参数才能透传;因为nginx仅在含响应体的成功状态码下执行add_header,而304无响应体,故跳过注入,导致x-frame-options等关键安全头缺失,存在被嵌入iframe或hsts中断等风险。

304 响应默认不带任何 add_header 定义的安全头,必须加 always 才能透传,否则浏览器会认为策略未声明,存在安全风险。
为什么 304 响应里安全头会消失
Nginx 的 add_header 默认只在“有响应体”的成功类状态码下生效,而 304 是个特例:它语义上表示“未修改”,响应体为空,Nginx 就跳过所有 add_header 指令 —— 即使文档说它在白名单里,实际行为是“不执行”。这意味着 X-Frame-Options、Strict-Transport-Security、Content-Security-Policy 这些头在 304 中完全缺失。
- 不是配置没生效,是 Nginx 主动跳过该阶段
- 哪怕后端(如 PHP)本身返回了这些头,304 响应也只含极简原始头(
Date、ETag、Cache-Control等),其他一概不带 - 浏览器收到无
X-Frame-Options的 304,可能允许被嵌入 iframe;无Strict-Transport-Security则 HSTS 策略中断
必须用 always,且每个 add_header 都得单独加
always 是唯一能覆盖 304 场景的参数,它让 Nginx 忽略状态码限制,在所有响应中强制注入。语法必须严格:add_header X-Frame-Options "DENY" always; —— always 要紧贴值后、分号前,不能加引号,也不能省略分号。
- 多个头要分别写
always:add_header X-Content-Type-Options "nosniff" always;和add_header Strict-Transport-Security "max-age=31536000" always; - 不能只在
http或server块写一次就完事:若请求经过rewrite last或try_files触发内部重定向,目标location必须也显式声明带always的add_header,否则不生效 - 如果用了
proxy_cache或fastcgi_cache,仅always不够:缓存必须存储并回传这些头,需检查是否误配了proxy_ignore_headers或fastcgi_ignore_headers
验证时别只看 200,重点抓 304 流量
用 curl -I 模拟协商缓存,确认头真出现在 304 响应里:
curl -I -H "If-None-Match: \"abc123\"" https://example.com/style.css
观察输出是否包含你配的 X-Frame-Options 等字段。若缺失,按顺序排查:
- 是否漏写
always(最常见) - 是否被同级或子
location中另一个add_header覆盖(Nginx 不继承,有则屏蔽上级) - 是否被
proxy_hide_header或fastcgi_hide_header显式过滤掉 - 是否用了 CDN 或前置代理,且未配置透传自定义头(比如要求设置
Vary: Origin)
最容易被忽略的是:304 响应头透传依赖于整个处理链路的每个环节都支持——从 Nginx 自身配置,到缓存模块,再到外部代理,缺一不可。光加 always 只是起点,不是终点。











