bigywater.tech
Flask · 133

安全响应头

安全响应头到底拦了什么?CSP 的 nonce 怎么用?HSTS 为什么不能无条件发?这篇用一个页面对照有无响应头,在真实的 Chrome 里逐个看哪些行为被拦住。

nonce
CSP 里允许特定内联脚本执行的随机值,每个响应都要不同
Referer=None
Referrer-Policy: no-referrer 时对方收到的来源
x_proto=1
让 request.is_secure 在 TLS 终止的代理后面正确判断 https

有无响应头的对比

实测 · 本机执行输出
  [没有安全响应头]
     内联脚本                         ran
     带 nonce 的内联脚本                ran
     MIME 是 text/plain 的外部脚本      ran
     收到的 Referer                  Referer=http://127.0.0.1:9601/
     被另一个站点用 iframe 嵌入            被嵌入的页面加载并回报了
  [加了 nosniff、X-Frame-Options、CSP(nonce)、Referrer-Policy]
     内联脚本                         没有执行
     带 nonce 的内联脚本                ran
     MIME 是 text/plain 的外部脚本      没有执行
     收到的 Referer                  Referer=None
     被另一个站点用 iframe 嵌入            没有回报(被拒绝嵌入)

上一篇:开发模式 Debugger 风险;下一篇:HTTPS 与反向代理。

浏览器是本机的 Google Chrome 无头模式。第一次运行时我把页面里的一个字符串引号写错、服务端没有启动,脚本因此卡住,我给 Chrome 的运行加了超时。

响应头本身

实测 · 本机执行输出
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-cdHD60VADsB8jdWt'; frame-ancestors 'none'; connect-src 'self' http://localhost
  Referrer-Policy: no-referrer

HSTS 与 is_secure

实测 · 本机执行输出
  http 请求: None | https 请求: max-age=31536000; includeSubDomains
  behind a TLS-terminating proxy, request.is_secure 是否为真取决于 ProxyFix(x_proto=1)(见第 135 篇);没配时即使用户走的是 https,这里也是 http
  无 ProxyFix,带 X-Forwarded-Proto: https → False
  ProxyFix(x_proto=1) → True

分步演示

安全响应头
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。本机 Google Chrome(无头模式)
    • 没有验证:HSTS 在浏览器里的实际效果(需要真实 HTTPS 站点)、CSP 的 report-uri 与 Report-Only 模式、Permissions-Policy 与 COOP/COEP、Flask-Talisman 之类封装库的默认值、CSP 对第三方脚本与样式的影响
    京ICP备2026067502号-1京公网安备11010502063487号