做Web服务这么多年,有件事我一直觉得挺魔幻的:很多团队会在服务器上花大力气配防火墙、上WAF、做各种渗透测试,但HTTP安全响应头这块却常常是空白。更要命的是,这种事不是小作坊才会犯,你去访问很多头部大厂的页面,打开浏览器的Network面板看一眼Response Headers,照样能挑出一堆问题——要么整个安全响应头一个都没配,要么配了CSP却漏了Permissions-Policy,要么HSTS的max-age短得跟开玩笑似的。
HTTP安全响应头不是一个多么复杂的东西,它就是服务器在HTTP响应里带的一组元数据,告诉浏览器要启用哪些安全保护机制。很多攻击之所以能得手,不是因为攻击者多高明,而是因为你的网站在响应头层面把防御的窗户全打开了。这篇文章我不整虚的,直接把我实际测试过的安全响应头清单、各个场景的配置方式、以及我踩过的坑全部整理出来,末尾会给一份可以直接复制粘贴的配置清单,你拿去照着改就行。
1. 为什么安全响应头总被当成"可选优化项"
1.1 安全响应头是什么,能挡住哪些攻击
先花两分钟把基础对齐。HTTP安全响应头不是某个框架的特性,也不是某个中间件的插件,它是HTTP协议本身的机制。服务器在返回响应时,往Response Headers里塞几个字段,浏览器读到这些字段后,就会执行对应的安全策略。这些策略组合在一起,相当于给浏览器装上了一圈主动防御的护甲。
举个最直观的例子:X-Frame-Options这个头。如果没有它,你的页面就能被别的网站用iframe嵌进去。攻击者做一个和你的登录页一模一样的透明iframe,叠在自己的恶意页面上,诱导用户输入账号密码,这就是典型的点击劫持。很多金融类网站的登录页至今还能被iframe嵌入,本质上就是X-Frame-Options没配或者配错了。
再比如CSP(Content-Security-Policy),它的作用类似给浏览器开了一份白名单,明确告诉浏览器脚本、样式、图片只能从哪些来源加载。如果攻击者往你的页面里注入了一段恶意脚本,而你的CSP又没有放开不可信来源,浏览器会直接拒绝执行这段脚本。这是目前防XSS最有效的一层防线,比后端各种过滤函数靠谱得多。
1.2 大厂漏配的三个真实原因
为什么大厂也会漏配?我分析下来,原因不外乎三个。
第一个是研发流程里根本没有这一环。大多数团队的开发流程是:写功能、联调、测试、上线。安全响应头不直接影响功能,漏了不会报错,也不会让用户打开页面出现异常,所以只要没有专门的安全团队卡上线流程,它就是那个永远没人管的角落。
第二个是框架默认配置不全。Spring Boot、Express、Django这些框架确实内置了一些默认安全头,但覆盖范围有限。比如框架不会帮你配CSP,因为要满足所有业务场景的CSP太困难了,框架不敢替你决定;Permissions-Policy同理。结果就是开发者以为框架帮忙兜底了,实际上兜了个寂寞。
第三个是多层架构下的信息断层。现代Web服务很少是单台服务器直接面对用户,前面通常还有Nginx、网关、CDN,响应头可以在任意一层加。负责网关的团队觉得头应该在应用层配,应用团队觉得网关层已经把安全做了吧,两边一互相甩锅,头就没了。更隐蔽的情况是CDN缓存:源站改了响应头,但CDN节点没回源,或者缓存的旧响应还在有效期内,用户拿到的还是旧头。
2. 我实测过的安全响应头清单:三层分级配置思路
2.1 第一梯队:不配等于裸奔
第一个必须配的是HSTS,全称Strict-Transport-Security。它的作用是告诉浏览器:以后访问我的域名只准用HTTPS,不要用HTTP,一次都不行。这个头防的主要是协议降级攻击和数据被明文劫持。比如用户在一个公共Wi-Fi下访问你的网站,如果没有HSTS,攻击者可以通过DNS劫持或中间人手段,把HTTPS请求偷偷降级成HTTP,用户在地址栏看到的可能还是那个网站,但传输的数据已经全是明文了。配置HSTS之后,只要浏览器访问过一次你的HTTPS页面,就会记住这个策略,后面哪怕用户手动在地址栏输入http://,浏览器也会自动跳转成HTTPS。
第二个是CSP。前面提到过,我再补充一个细节:CSP的粒度可以非常细,细到"这个页面只允许从你自己的域名加载脚本,脚本的src属性不允许使用data:协议,不允许使用eval()"。配置得当的情况下,反射型XSS和存储型XSS的有效性会大幅降低。常见的配置指令有default-src、script-src、style-src、img-src、connect-src、frame-ancestors,每个指令都可以单独声明允许的来源。
第三个是X-Frame-Options。它的配置非常简单,安全取值就两个:DENY表示任何网站都不能嵌入你的页面;SAMEORIGIN表示只有同源页面能嵌入。如果你不确定业务上有没有被第三方合法嵌入的需求,就先用SAMEORIGIN。这个头现在和CSP的frame-ancestors指令存在功能重叠,但它在老浏览器上的兼容性更好,所以依然值得保留。
2.2 第二梯队:几行配置消除低级漏洞
第二梯队这几个响应头,单个看起来都很小,但组合在一起能消灭一大片低级漏洞。
X-Content-Type-Options专门对付MIME类型嗅探。正常情况下,服务器通过Content-Type告诉浏览器响应是什么类型。但老版本浏览器有个毛病,会去嗅探响应内容来猜测类型,比如你明明返回的是text/plain,浏览器看内容长得像HTML,就自作主张当成HTML执行了。攻击者可以利用这点绕过上传限制,上传一个内容像HTML但Content-Type是其它类型的文件,诱导浏览器执行。配置X-Content-Type-Options: nosniff之后,浏览器会严格信任服务器返回的Content-Type,不做任何猜测。
Referrer-Policy控制的是浏览器在页面跳转时,把当前页面的URL信息带到目标网站这个行为。默认的no-referrer-when-downgrade在大多数情况下够用,但我更推荐strict-origin-when-cross-origin:同源请求带完整URL,跨源请求只带origin,且HTTPS页面跳HTTP时不带任何Referrer。这样既保证了业务上需要的Referrer信息,又避免把URL里的敏感参数(比如token、sessionId)泄露给第三方网站。
Permissions-Policy这个头在不少团队的配置清单里是缺席的。它的作用是限制浏览器功能,比如你的网站根本不用麦克风,那就直接声明microphone=(),这样页面上任何脚本都无法调用麦克风API。类似的功能还有geolocation(定位)、camera(摄像头)、usb(USB设备)等。你不声明的功能,默认也有可能被某些脚本调用,显式声明禁用是一种防御性的做法。
2.3 第三梯队:跨源隔离,很多人听都没听过
第三梯队这三个响应头属于进阶玩法,核心是围绕跨源隔离(Cross-Origin Isolation)做的,很多团队的运维和前端甚至没听过。
Cross-Origin-Opener-Policy(COOP)控制的是:当前页面通过window.open或target=_blank打开的新页面,以及它们之间的反向关联。如果没有COOP,一个恶意站点可以通过引用你的页面拿到window.opener,然后修改location、读取敏感信息。配置COOP: same-origin之后,跨源的opener关系会被彻底切断。
Cross-Origin-Resource-Policy(CORP)控制的是:别的站点能不能加载你站点的资源。如果你这个站点是纯API或者纯资源站,不打算给任何第三方用,可以配置CORP: same-site或same-origin。这样即便有人在自己的页面里通过