给网站加上 Content-Security-Policy: default-src 'self',可能立即让远程字体、图片和内联脚本失效。安全策略需要约束真实资源图,而不是贴一段看起来严格的字符串。先知道页面在执行什么,再决定哪些执行应被允许。
本文针对主要由静态 HTML 构成的网站。CSP 能减少某些注入造成的影响,却不能修复不安全的数据拼接,也不能替代输出编码、权限验证或依赖审查。
用 Report-Only 建立实际资源清单
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
报告模式不会强制阻断,但浏览器会记录不符合策略的加载。接收报告需要配置适当端点与报告指令,并考虑浏览器支持差异;只设置上面这条头,不会凭空出现一个服务端报告库。开发时可以先通过控制台查看,再建立受控采集。
报告可能包含访问 URL、资源地址等信息,应限制采集范围和保留时间。不要把报告端点变成无限接收任意 JSON 的存储接口。对重复噪声做聚合,区分浏览器扩展注入与站点自身行为,否则容易为扩展产生的违规放宽本站策略。
假设文章站使用同源脚本、一个字体服务、一个图片 CDN 和一个分析接口。应分别登记资源类型、加载页面、业务用途及负责人,而不是只维护一串域名。字体样式表与实际字体文件可能来自不同来源,允许样式域名并不自动允许字体文件;分析脚本可以加载,也不意味着其网络上报能够通过 connect-src。每类请求的职责不同,策略也应该分别说明。
资源图还能揭示第三方依赖的扩张。一个视频组件可能进一步加载图片、子框架和脚本,插件升级后又加入新域名。如果为了让它工作直接允许全部 HTTPS 来源,CSP 的约束价值就被削弱。需要判断哪些功能必须保留,哪些可以改为普通链接或同源静态内容。少量第三方交互可以独立页面承载,不必让每篇文章都接受相同宽泛权限。
报告中的来源不等于可信来源。浏览器扩展、恶意输入和失效资源都可能产生违规。自动从日志扩充允许列表,会把攻击尝试变成权限申请成功。应按来源分类、确认是否为预期加载,再由代码审查修改策略。对于未知来源,先复现加载链路,不能仅根据出现频率判断合法性。
资源权限要按用途拆开
img-src 可以允许特定图片 CDN,connect-src 约束脚本请求接口,frame-src 约束嵌入页面。frame-ancestors 则控制谁可以嵌入本站,通常必须通过响应头发送,不是普通 HTML meta 可以完整替代的能力。
如果站点需要被特定合作方嵌入,frame-ancestors 'none' 就不合适。策略不是越短越安全,而是越贴近业务边界越可检验。支付、登录和视频组件可能在不同域名之间跳转,应使用真实流程检查,不要只验证首页。
供应商文档要求加入某个脚本域名时,记录其对应组件、适用页面和替代方案。供应商改域名应作为依赖变更审查,而不在生产故障时无条件添加通配符。若一个组件要求允许任意内联脚本,应先确认有没有无内联版本、哈希方案或隔离页面。功能选择本身也能缩小安全权限,不是所有兼容问题都必须靠放宽策略解决。
对于需要被嵌入的页面,可以单独设置祖先来源,而普通文章继续禁止嵌入。对于需要用户上传图片的页面,资源应由受控地址提供,不能因此允许任意可执行来源。权限按用途和页面范围限定,减少一个特殊功能扩大全站攻击面的机会。
策略维护者还需要知道安全测试的限制。浏览器中观察到某个样本被阻断,只证明该样本在该策略和浏览器下的行为,不覆盖全部注入形式。将样本放入自动测试有价值,但仍应审查输出拼接与依赖加载。出现新的违规分类时,先判断其能力要求是否合理,再修改策略,而不是把“没有报告”当成唯一上线标准。
内联代码需要明确处理方式
静态页面有固定内联脚本,可以使用哈希允许对应内容。脚本字节变化后哈希必须更新,空白变化也可能影响匹配。构建工具如果压缩或注入脚本,哈希应针对最终产物计算,不能针对源文件猜测。
动态页面常使用每个响应生成的 nonce,并同时写入响应头和允许的脚本标签。不能把固定 nonce 写进静态 HTML 后长期复用,那不具备预期隔离作用。对纯静态站点,把少量内联逻辑移到同源外部脚本往往更易维护,但仍需防止攻击者控制允许源上的可执行文件。
'unsafe-inline' 会明显放宽限制,不应因为遇到一个兼容问题就长期保留。内联样式与脚本是不同指令,修复样式加载不应顺手放开脚本执行。第三方组件提出的策略需求也应逐条审查,不能原样加入所有通配域名。
若页面存在固定主题初始化脚本,构建器可能改变空白或插入属性。哈希针对脚本内容字节生成,再写入对应响应策略。需要让 HTML 与头文件在同一次构建中产生,避免脚本更新后策略仍然指向旧哈希。开发服务器会额外加载热更新脚本,因此开发环境策略不能直接作为生产验收结果。
不同页面若包含不同内联脚本,策略可以按页面生成,也可以使用有限哈希集合。全站集合便于部署,却会扩大每页允许执行的内容;按页策略更精确,但增加输出规则数量。个人静态站常可将必要逻辑移到同源外部文件,降低同步维护负担。无论选择哪种方式,都应检查允许源上的上传内容是否可能被当作脚本执行。
动态 nonce 必须不可预测并与当前响应一致。若缓存 HTML 与响应头来自不同版本,nonce 不匹配会阻断正常功能;若为缓存便利把 nonce 固定,则失去应有的保证。动态页面、边缘缓存和 nonce 的组合需要完整设计,不能把一段 nonce 生成函数插入静态项目就宣称完成保护。
策略组合不是后来的头覆盖前面的头
同一响应出现多条 CSP 时,浏览器会按适用规则执行多项策略,不能假设更宽松的新策略取消旧策略。应用、代理和托管平台各自设置头,可能造成某个资源满足其中一条却被另一条拒绝。验收应查看实际响应头,不只查看源代码里的配置文件。报告模式与强制模式也分别存在,报告到达不代表资源已经被阻断。
有些指令不继承 default-src 的缺省行为,必须查看各指令的规范与文档。不要用“默认只允许自己”推导所有导航、表单和嵌入都已被限制。表单提交目标与脚本连接目标是不同能力,需要根据真实业务配置。登录和支付流程尤其要测试用户交互后的跳转,而不只是页面首屏资源加载。
meta 形式存在功能限制,而且文档解析时机也影响保护范围。需要 frame-ancestors 等响应级能力时,应在部署层发送头。服务工作线程、错误页面和重定向响应是否有策略也要明确,不能只给成功 HTML 加头。保护范围由浏览器实际收到的响应决定,不由后台某个勾选框决定。
报告端点需要自己的安全与隐私设计
报告可能携带完整页面 URL,查询中有令牌或用户输入时就可能形成敏感日志。应在服务端清理不必要参数、限制字段长度和请求体积,并设置保留期。端点接受来自浏览器的输入,不能信任其声明的页面来源,也不能让报告直接触发外部请求。恶意客户端可以伪造大量报告,聚合与限流属于基本要求。
统计时按指令、页面类型、代码版本和浏览器区分,避免把一次扩展注入重复上报当成站点回归。报告数量要有分母,例如覆盖的页面访问,而不是只看绝对数量。浏览器支持差异和报告发送条件会让收集不完整,未收到报告不能证明没有违规。因此日志与主动测试必须结合,而不是等待生产用户替团队测试。
对于私人页面,尽量减少报告内容,必要时只保留分类与版本。外部分析平台的保留策略也需要确认。安全观测不应该因收集过量上下文而制造新的泄漏面。即使报告模式不阻断用户操作,端点故障和日志成本也可能影响系统,部署前应进行资源上限测试。
把构建产物当作策略输入
一次构建升级可能把外部脚本合并成内联块,或者让原本内联的初始化逻辑变成动态导入。源代码里的资源清单没有变化,浏览器实际执行图却变了。策略审查应该基于最终 HTML 和脚本入口,而不仅搜索源码中的 script 标签。构建报告可以列出内联块、外部来源与动态加载依赖,并将差异交给维护者确认。
对固定脚本哈希,生成器读取最终产物中的实际脚本文本,产生策略,再验证生成的 HTML 与策略是否相互对应。不要经过额外格式化后再计算,浏览器比较的对象不是格式化后的审阅版本。缓存头文件与 HTML 也应属于同一版本,部署只更新其中一项会阻断合法脚本。测试让脚本文本改变一个字节,确认旧策略不再许可它,新策略可以正常运行。
外部脚本采用同源不等于无风险。如果同源有用户可控的上传路径,或者存在返回可执行 JavaScript 的接口,允许整个 self 的实际权限就比看起来更大。应隔离上传来源、设置正确内容类型并审查可执行入口。CSP 与上传隔离相互补充,不能让一个允许源同时承载任意用户内容,再把来源允许称为充分的执行约束。
动态加载链也要有明确所有者。一个可信脚本继续创建其他脚本时,采用哪些 CSP 能力取决于实际策略和浏览器支持;例如严格动态信任相关机制会改变主机允许列表的作用。没有这些需求时不必堆叠高级指令。需要时则用官方规范与浏览器测试解释其效果,不把从供应商复制来的字符串视为不需要理解的安全配置。
为第三方组件写一份能力申请
假设视频组件提出脚本、框架、图片和连接四类来源需求。能力申请列出每类请求用途、页面范围、是否必须登录、失败时的替代入口与移除方式。审核不是检查供应商有多知名,而是这些能力是否与本站任务匹配。文章页只需要展示视频链接时,完整嵌入组件可能没有足够收益承担四类长期权限。
第三方依赖升级后重新比较请求图,特别关注新连接来源和更宽的内联要求。可以在预览中阻断新来源观察组件哪项功能失败,但不要据此随意保留一个不完整且误导性的控件。要么授权明确能力并验证,要么提供可靠替代方案。限制权限同时保留任务完成路径,比让组件看起来存在却无法播放更合理。
移除组件也应删除不再使用的允许项,并保留一个负向样本证明该来源不能继续执行。权限累积是长期维护问题,只新增不删除会让策略逐渐失去约束。对于多个页面共享策略的站点,先核对其他页面仍是否依赖该来源;更细的页面策略能缩小权限,但增加配置与部署复杂度。取舍应写进资源记录,而不是由一次线上故障决定。
用报告分析避免错误扩大允许列表
收到大量 blocked-uri 事件时,先按指令、页面类型与版本聚合,再抽样检查对应加载链。浏览器扩展注入常表现为特定扩展来源,攻击输入可能表现为未知远程域名,正常回归可能集中在新版本与某个组件。频率只是分类线索,不是授权证据。一个攻击者可以制造高频报告,不能让计数触发自动白名单更新。
报告内容也不是浏览器身份凭证。端点可被任意客户端直接调用,字段应校验大小、类型和允许格式。不要根据报告中的资源 URL 主动下载资源调查,那会把日志接收器变成服务端请求入口。人工复核需要访问外部地址时使用受控环境与安全工具,保持采集与调查职责分开。
比较违规数量时,需要估算采样覆盖及页面访问分母。报告减少可能因为策略修复,也可能因为端点失效、浏览器不支持或采样改变。主动测试发现端点不可达时,应单独报警,不把零报告当作安全胜利。报告保留期到期后删除不必要的 URL 和样本文本,聚合趋势可以长期保留,但不需要长期保存用户输入。
明确强制执行失败时的恢复动作
如果主题按钮被阻断,先确认是脚本哈希不匹配、实际头组合冲突还是资源部署缺失。三种原因对应不同修复:重新同步产物、移除错误重复配置或恢复脚本文件。直接加 unsafe-inline 会把一个部署问题变成长期权限放宽。恢复速度重要,但应恢复上一份已验证配置,随后在预览复现再修正。
如果登录或支付流程被阻断,应暂停受影响入口并提供真实状态,而不是让用户反复提交。CSP 通常约束加载与执行,不提供业务副作用撤销;某个请求可能已经提交成功,只是后续页面打不开。恢复界面需要查询真实业务结果,避免把视觉失败当作交易失败。安全头故障与业务结果未知分开处理,能减少重复付款或重复操作。
验收报告记录支持的浏览器、实际响应策略、正向任务结果与负向阻断样本。对于未验证的平台如实留空,不把标准文档支持推导为项目必然正常。策略的上线完成意味着已验证范围内必要功能可用且越权能力受限,而不是宣称所有注入风险已经消失。
从一个页面开始强制执行
先让报告模式运行覆盖主要浏览器与页面类型,再对简单页面启用强制策略。检查导航、搜索、图片、字体、分析脚本和错误页面。构建后的 HTML 与开发环境可能不同,必须在部署预览环境测试。
上线时保留回滚路径。发现违规要判断是代码回归、第三方变更还是攻击尝试,不能自动把违规来源加入允许列表。对 CI 增加资源域名与内联脚本变化检查,可以让安全策略与页面修改同时接受审查。
验收的结果应是“必要功能正常、非授权脚本被阻断”,不是控制台里没有任何警告。CSP 的价值来自持续维护的资源契约,放宽到允许所有来源后再宣称已经开启保护,没有实际意义。
还要把元素内联与属性内联分开。允许一个固定 script 元素的哈希,并不意味着按钮上的 onclick 也获得同样许可。若组件库仍生成事件属性,优先迁移到已批准脚本里的事件绑定,不为兼容少量旧按钮扩大所有内联执行权限。测试同一按钮的正常绑定版本与合成事件属性版本,分别记录用户动作和违规类别。
样式也有类似区别:固定 style 元素与运行时设置 style 属性是两种输出。支持相应 CSP 指令的浏览器可按元素和属性表达更细权限,但项目必须核对目标浏览器与具体指令的回退规则。一个图表通过修改 CSS 类实现状态,另一个通过不断写入 style 属性实现布局,不能仅因为它们都叫“样式”就期待相同政策适用。
这项检查从最终 DOM 找来源:是作者 Markdown、组件模板还是第三方初始化脚本生成了属性?先定位生成者,再决定改输出还是授权。若只能在某个图表页面保留必要能力,记录页面范围及替代方案,而普通文章维持原约束。测试结果只能说明已覆盖输出形态,不把一次按钮可用解释为全部动态样式已兼容。
参考:MDN CSP、CSP Level 3。