内容安全策略告诉支持它的浏览器,一个文档允许哪些动作。获取资源类指令分别控制脚本、样式、图片等类别,其他指令则负责不同边界。策略通常通过文档响应头提供,为浏览器行为增加约束,但不会修复注入漏洞、认证应用用户,也不会保证所有可能的脚本执行方式都已消失。应把它理解为具有具体作用范围的一层防护,而不是只要字段存在就获得完整安全保证。
缺失某个指令并不统一表示禁止。一些获取资源类指令会回退到默认来源指令,而嵌入父级、基础地址和表单目标等边界需要分别考虑。只包含脚本限制的策略,并不会自动完整限制图片加载、页面嵌入和表单提交。审阅时要逐项理解指令范围,尤其不能把默认来源写成全部拒绝后,就假定那些不继承该默认值的功能也被关闭。
为真正需要运行的脚本授权 随机数策略为经过明确选择的脚本元素附加不可预测的值,并在响应策略中声明同一个值。每次响应应生成新的随机数,只把它写到服务端确实准备授权的脚本上。如果自动给发现的每一个脚本元素都补上随机数,被注入的脚本也可能同时得到许可。构建时生成一次、以后持续复用的固定值,也失去了这种设计所依赖的逐响应不可预测性。
同一条来源指令中的表达式通常表示多个可选条件。把允许的主机与随机数写在一起,并不意味着脚本必须同时满足这两个条件。对于内容固定的静态页面,可以使用经过批准脚本的内容哈希,脚本精确内容发生变化时同步更新策略。需要根据页面怎样生成来选择方案;随意加入宽泛的内联执行许可,可能直接撤销原本希望通过随机数建立的限制。
坏掉的样子 开始强制执行后,合法的内联脚本、事件处理器、动态生成内容或此前没有注意到的依赖,都可能突然失效。仅报告模式能够在允许动作继续发生的同时观察违规,所以页面在该模式下正常运行,并不能证明强制策略也会允许它。报告还需要按含义解释:某条图片违规不能证明目标内联脚本被拦截,报告数量少也不能替代对关键页面功能的实际检查。
多份强制策略会共同生效。代理新增一份较宽松的响应头,并不会放宽已经存在的严格策略,因为动作必须满足每一份适用的强制策略。这可以解释为什么一个看起来正确的随机数仍然无法让脚本执行。应该检查最终交付的全部策略头和浏览器报告的有效指令,再修正真正需要的边界或合法资源用法,不能假定最后出现的字段自然覆盖此前所有限制。
快速验证 保存代码为 csp-nonce.py,执行 python3 csp-nonce.py –serve,在打印出的地址后加上 ?mode=enforce 并打开。本次检查使用 Python 3.14.4 和 Chrome 152。服务端为每份文档生成新的随机数,并故意让一个脚本没有该值。本机回环报告接收端只接收并丢弃正文,页面展示的证据来自浏览器违规事件。结束后按控制键加 C 关闭服务,不需要连接任何外部报告平台。
import base64 import json import secrets import sys from http.server import BaseHTTPRequestHandler, HTTPServer from threading import Event, Thread from urllib.parse import parse_qs, urlsplit from urllib.request import urlopen class Handler(BaseHTTPRequestHandler): def log_message(self, *args): pass def do_POST(self): self.rfile.read(int(self.headers.get("Content-Length", "0"))) self.send_response(204) self.send_header("Content-Length", "0") self.end_headers() def do_GET(self): mode = parse_qs(urlsplit(self.path).query).get("mode", ["enforce"])[0] assert mode in {"enforce", "report", "intersect"} nonce = base64.b64encode(secrets.token_bytes(18)).decode() policy = ("default-src 'none'; script-src 'nonce-" + nonce + "'; " "base-uri 'none'; object-src 'none'; report-uri /reports") body = ("""<!doctype html><meta charset="utf-8"><title>CSP nonce lab</title> <h1>Nonce, report-only, and policy intersection</h1> <pre id="result">If both scripts are blocked, this text stays unchanged.</pre> <script nonce=""" + '"' + nonce + '"' + """> window.allowed=true; window.violations=[]; document.addEventListener('securitypolicyviolation',event=>{ window.violations.push({directive:event.effectiveDirective,disposition:event.disposition,blocked:event.blockedURI}); }); </script> <script>window.unapproved=true;</script> <script nonce=""" + '"' + nonce + '"' + """> setTimeout(()=>{ window.labResult={allowed:window.allowed===true,unapproved:window.unapproved===true,violations:window.violations}; document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2); },100); </script>""").encode() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.send_header("Content-Security-Policy-Report-Only" if mode == "report" else "Content-Security-Policy", policy) if mode == "intersect": self.send_header("Content-Security-Policy", "script-src 'none'") self.end_headers() self.wfile.write(body) server = HTTPServer(("127.0.0.1", 0), Handler) thread = Thread(target=server.serve_forever, daemon=True) thread.start() origin = f"http://127.0.0.1:{server.server_port}" try: policies = [] for mode in ("enforce", "enforce", "report", "intersect"): with urlopen(origin + "/?mode=" + mode, timeout=5) as response: key = "Content-Security-Policy-Report-Only" if mode == "report" else "Content-Security-Policy" values = response.headers.get_all(key) assert len(values) == (2 if mode == "intersect" else 1) assert "'nonce-" in values[0] policies.append(values[0]) response.read() assert policies[0] != policies[1] print(json.dumps({"app_url": origin, "nonce_changes_per_response": True, "wire_policy_checks": "PASS"}), flush=True) if "--serve" in sys.argv: Event().wait() except KeyboardInterrupt: pass finally: server.shutdown() thread.join(timeout=5) server.server_close() 强制模式中,获准脚本应把 allowed 设为真,unapproved 则保持为假。相关违规事件的有效指令是 script-src-elem,被阻止的对象是内联脚本,处置方式是强制执行。将地址改为 ?mode=report 后,两个脚本标志都变为真,而违规处置方式变成报告。浏览器还可能记录其他资源违规,因此应按事件字段定位目标问题,不应把报告总数固定为一条,再据此误判执行结果。
...