Web 安全头部与 CSP 策略
概述
Web 安全头部(Security Headers)是 HTTP 响应头的重要组成部分,它们为浏览器提供了额外的安全指令,帮助抵御各类 Web 攻击。通过合理配置安全头部,网站可以显著降低跨站脚本攻击(XSS)、点击劫持、MIME 类型混淆、协议降级攻击等风险。内容安全策略(Content-Security-Policy,简称 CSP)则是其中最为强大也最为复杂的机制,它允许开发者精确控制浏览器可以加载和执行哪些资源。
本文将系统性地介绍各类 Web 安全头部的原理、配置方法及最佳实践。
Content-Security-Policy(CSP)
CSP 基本原理
CSP 通过 HTTP 响应头 Content-Security-Policy 告知浏览器一组策略规则,浏览器据此决定是否允许加载或执行特定资源。即使攻击者成功注入了恶意脚本,CSP 也能阻止其执行,因此 CSP 被认为是防御 XSS 的"最后一道防线"。
CSP 策略由一系列指令(directive)组成,每个指令指定一个或多个来源(source)。基本语法如下:
Content-Security-Policy: directive-1 source-1 source-2; directive-2 source-3核心指令详解
default-src
default-src 是 CSP 的兜底指令,当某个资源类型没有设置专门指令时,浏览器会回退使用 default-src 的值。例如,如果只设置了 default-src 'self' 而没有设置 script-src,则脚本资源也默认只能从同源加载。
Content-Security-Policy: default-src 'self'需要注意的是,script-src 和 style-src 有特殊的回退规则——如果它们未被显式设置,且 default-src 也未设置,则浏览器对所有来源放开限制。因此,始终建议显式设置 default-src。
script-src
script-src 控制 JavaScript 资源的加载和执行来源,是 CSP 中最为关键的指令。以下是一些常见配置示例:
# 仅允许同源脚本
Content-Security-Policy: script-src 'self'
# 允许同源和指定 CDN
Content-Security-Policy: script-src 'self' https://cdn.example.com
# 允许内联脚本(不推荐,会削弱防护效果)
Content-Security-Policy: script-src 'self' 'unsafe-inline'注意
'unsafe-inline' 会允许所有内联脚本执行,极大削弱 CSP 的防护能力。生产环境中应尽量使用 nonce 或 hash 机制来替代 'unsafe-inline'。
style-src
style-src 控制 CSS 样式资源的加载来源,包括外部样式表、内联样式以及样式属性。
Content-Security-Policy: style-src 'self' https://fonts.googleapis.com与 script-src 类似,'unsafe-inline' 可用于允许内联样式,但同样会降低安全性。推荐使用 nonce 或 hash 机制。
img-src
img-src 控制图片资源的加载来源。由于图片加载可能被用于数据泄露(例如通过图片 URL 参数携带信息),合理限制图片来源也很重要。
# 仅允许同源图片
Content-Security-Policy: img-src 'self'
# 允许同源和 HTTPS 图片
Content-Security-Policy: img-src 'self' https: data:
# 允许同源和指定图床
Content-Security-Policy: img-src 'self' https://images.example.comconnect-src
connect-src 控制通过脚本发起的网络连接请求,包括 fetch()、XMLHttpRequest、WebSocket、EventSource 等。限制 connect-src 可以有效防止数据外泄。
# 仅允许请求同源 API
Content-Security-Policy: connect-src 'self'
# 允许请求同源和指定 API 服务器
Content-Security-Policy: connect-src 'self' https://api.example.com wss://ws.example.comframe-src 与 child-src
frame-src 控制 <iframe>、<frame> 和 <object> 标签可以加载的来源。child-src 是较旧的规范,现代浏览器已推荐使用 frame-src 替代。
# 禁止所有嵌入
Content-Security-Policy: frame-src 'none'
# 仅允许嵌入同源页面
Content-Security-Policy: frame-src 'self'
# 允许嵌入指定第三方服务
Content-Security-Policy: frame-src 'self' https://www.youtube.comCSP Nonce 与 Hash 机制
Nonce 机制
Nonce(Number Used Once)是一个一次性随机值,服务器在每次响应中生成一个唯一的 Base64 编码字符串,将其同时设置在 CSP 头部和内联脚本/样式的 nonce 属性中。浏览器比对两者是否匹配,从而决定是否执行该内联内容。
服务端配置示例:
Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'对应的 HTML 中使用:
<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">
console.log('这个脚本会被执行');
</script>Nonce 的核心要求是每次 HTTP 响应都必须生成全新的随机值,不可复用。如果攻击者无法预测 nonce 值,就无法注入可执行的脚本。
Hash 机制
Hash 机制允许开发者将信任的内联脚本或样式的哈希值直接写入 CSP 头部。浏览器在加载内联内容时计算其哈希值,仅当与策略中的哈希值匹配时才执行。
Content-Security-Policy: script-src 'sha256-abc123...' 'sha384-def456...'Hash 的优势在于无需修改 HTML 模板即可生效,适合静态页面或代码审查严格的场景。但缺点是每当内联脚本内容发生变化时,都需要同步更新 CSP 头部的哈希值。
选择建议
| 场景 | 推荐方案 |
|---|---|
| 动态生成 HTML,内联脚本频繁变化 | Nonce |
| 静态页面,内联脚本极少变化 | Hash |
| 需要加载外部脚本 | 将外部域名加入白名单 |
strict-dynamic 关键词
strict-dynamic 是 CSP Level 3 中引入的机制,旨在解决传统白名单模式在大型项目中的维护难题。当在 script-src 中指定 'strict-dynamic' 后,浏览器允许被受信任脚本动态加载的任何脚本执行,而不再依赖域名白名单。
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'上述策略的含义是:
- 只有带有正确 nonce 的脚本可以执行。
- 该脚本通过
appendChild、document.createElement('script')等方式动态创建的其他脚本也被信任。 - 浏览器忽略传统的域名白名单(如
'self'在 strict-dynamic 模式下对脚本可能无效)。
strict-dynamic 的巨大优势是解决了单页应用(SPA)和模块加载器中脚本动态加载的 CSP 兼容性问题。但需要注意,strict-dynamic 在较旧浏览器中不受支持,建议结合 'unsafe-inline' 作为降级方案:
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic' 'unsafe-inline'由于旧浏览器不认识 'strict-dynamic' 而回退到 'unsafe-inline',新浏览器则会忽略 'unsafe-inline' 并执行 strict-dynamic 规则。
CSP 报告与 Report-Only 模式
report-to 与 report-uri
CSP 支持将违规行为报告到指定的服务器端点,便于开发者监控和调试策略。
# report-uri(已废弃但广泛支持)
Content-Security-Policy: default-src 'self'; report-uri https://example.com/csp-report
# report-to(CSP Level 3 标准)
Content-Security-Policy: default-src 'self'; report-to csp-endpoint对应的 Report-To 头部需要配置报告端点:
Report-To: {"group":"csp-endpoint","max_age":86400,"endpoints":[{"url":"https://example.com/csp-report"}]}Report-Only 模式
Content-Security-Policy-Report-Only 是 CSP 的"观察模式"——浏览器仅报告违规行为而不阻止执行。这非常适合在全面启用 CSP 之前进行灰度测试。
Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://example.com/csp-report通过在响应头中同时设置 Content-Security-Policy(强制执行)和 Content-Security-Policy-Report-Only(仅报告),可以分段推进策略收紧。
报告格式
CSP 违规报告以 JSON 格式发送,包含以下关键字段:
| 字段 | 说明 |
|---|---|
document-uri | 违规发生的页面 URL |
violated-directive | 被违反的指令名称 |
blocked-uri | 被阻止加载的资源 URI |
effective-directive | 实际生效的指令名称 |
original-policy | 完整的原始策略字符串 |
HSTS(Strict-Transport-Security)
原理与作用
HSTS 头部告诉浏览器:该域名只能通过 HTTPS 访问,任何尝试通过 HTTP 访问的请求都应被浏览器自动升级为 HTTPS。这可以有效防御中间人攻击(MITM)和 SSL 剥离攻击。
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload指令说明
| 指令 | 说明 |
|---|---|
max-age=<秒> | 浏览器记住此策略的时长,推荐至少 31536000(1 年) |
includeSubDomains | 策略是否应用于所有子域名 |
preload | 申请将域名加入浏览器 HSTS 预加载列表 |
部署注意事项
- 初始部署时先从短
max-age开始,确认无问题后再逐步延长。 includeSubDomains需谨慎使用——确保所有子域名都已支持 HTTPS,否则将导致子域名不可访问。- HSTS preload 需要提交到 hstspreload.org,一旦加入便很难移除,务必在充分测试后进行。
X-Frame-Options 与 frame-ancestors
X-Frame-Options
X-Frame-Options 是防御点击劫持(Clickjacking)的传统头部,控制页面是否允许被嵌入 <iframe> 中。
# 禁止被任何页面嵌入
X-Frame-Options: DENY
# 仅允许被同源页面嵌入
X-Frame-Options: SAMEORIGINframe-ancestors(CSP 替代方案)
frame-ancestors 是 CSP 提供的更灵活的替代方案,不仅可以支持多个域名,还能与 CSP 报告机制结合。
Content-Security-Policy: frame-ancestors 'self' https://trusted-site.com当两者同时存在时,frame-ancestors 优先级更高,浏览器会忽略 X-Frame-Options。推荐使用 frame-ancestors,因为它的表达能力更强。
| 特性 | X-Frame-Options | frame-ancestors |
|---|---|---|
| 支持多个来源 | 否 | 是 |
| 支持通配符 | 否 | 是 |
| 支持 CSP 报告 | 否 | 是 |
| 浏览器兼容性 | 广泛 | 较新 |
X-Content-Type-Options
原理
X-Content-Type-Options 用于禁止浏览器的 MIME 嗅探(MIME sniffing)行为。当服务器返回的资源没有明确指定 Content-Type 或类型有歧义时,浏览器会尝试通过分析内容来自动推断类型。攻击者可利用此行为诱导浏览器将非脚本文件当作脚本执行,导致 XSS 攻击。
X-Content-Type-Options: nosniff设置 nosniff 后,浏览器严格遵循服务器返回的 Content-Type,不再进行类型猜测。这个头部虽然简单,却是 Web 安全中最基本也最容易被忽视的防御措施之一。
最佳实践
始终在应用的每个响应中添加此头部,并与正确的 Content-Type 配合使用:
Content-Type: application/json
X-Content-Type-Options: nosniffReferrer-Policy
原理
Referrer-Policy 控制 HTTP 请求中 Referer 头部携带的信息量。当用户从一个页面跳转到另一个页面时,Referer 会携带来源页面的 URL,这可能泄露路径参数、查询字符串等敏感信息。
常用策略值
| 策略值 | 行为 |
|---|---|
no-referrer | 任何情况下都不发送 Referer |
same-origin | 同源请求发送完整 Referer,跨源请求不发送 |
strict-origin | HTTPS→HTTPS 发送来源域名,HTTPS→HTTP 不发送 |
strict-origin-when-cross-origin | 推荐默认值:同源发送完整 URL,跨源 HTTPS 仅发送来源域名,降级到 HTTP 时不发送 |
origin-when-cross-origin | 同源发送完整 URL,跨源仅发送来源域名 |
Referrer-Policy: strict-origin-when-cross-origin推荐将 strict-origin-when-cross-origin 作为默认策略,它在功能完整性和隐私保护之间取得了良好平衡。
Permissions-Policy / Feature-Policy
概述
Permissions-Policy(原 Feature-Policy)允许网站控制浏览器 API 和功能的启用与禁用。通过精确控制摄像头、麦克风、地理位置、通知等敏感 API 的访问权限,可以有效减少攻击面。
# 禁止所有敏感 API
Permissions-Policy: camera=(), microphone=(), geolocation=()
# 仅允许同源使用 API
Permissions-Policy: camera=(self), microphone=(self)
# 允许指定域名使用 API
Permissions-Policy: geolocation=(self "https://maps.example.com")常见控制的功能
| 功能 | 说明 |
|---|---|
camera | 摄像头访问 |
microphone | 麦克风访问 |
geolocation | 地理位置信息 |
notifications | 通知推送 |
payment | Web 支付 API |
usb | USB 设备访问 |
fullscreen | 全屏 API |
autoplay | 自动播放音视频 |
clipboard-read | 剪贴板读取 |
clipboard-write | 剪贴板写入 |
推荐采用最小权限原则:只开放业务必需的 API,其余一律禁用。
Trusted Types
原理
Trusted Types 是较新的 Web 安全机制,旨在从根本上消除基于 DOM 的 XSS 攻击。它的核心思想是:任何将字符串传递给可能被执行的危险 DOM API(如 innerHTML、document.write、eval 等)的行为都必须经过一个"净化"(sanitization)函数。
启用 Trusted Types:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types my-policy使用示例
启用后,以下代码会被浏览器阻止:
// 浏览器会抛出 TypeError,阻止执行
element.innerHTML = '<img src=x onerror=alert(1)>';必须通过 Trusted Types 策略创建安全的 HTML:
const sanitized = trustedTypes.createPolicy('my-policy', {
createHTML: (input) => {
return DOMPurify.sanitize(input); // 使用净化库
}
});
element.innerHTML = sanitized.createHTML('<img src=x onerror=alert(1)>');兼容性
Trusted Types 目前在 Chrome 和 Edge 中受支持。可以通过以下 CSP 进行灰度发布:
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri https://example.com/report在确认无违规报告后再切换为强制执行模式。
Nginx 配置示例
以下是一个综合配置 Nginx 安全头部的示例:
server {
listen 443 ssl;
server_name example.com;
# SSL 配置
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# CSP
add_header Content-Security-Policy "
default-src 'self';
script-src 'nonce-$request_id' 'strict-dynamic';
style-src 'self' 'nonce-$request_id';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-src 'none';
frame-ancestors 'none';
report-uri https://example.com/csp-report;
" always;
# 其他安全头部
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Trusted Types
add_header Content-Security-Policy "require-trusted-types-for 'script'; trusted-types default" always;
location / {
proxy_pass http://localhost:3000;
}
}说明
上述配置使用 $request_id 作为 nonce 值,Nginx 会为每个请求生成唯一标识。实际生产环境中建议使用专门的 nonce 生成模块或由后端应用生成。
Spring Security 配置示例
在 Spring Security 中,可以通过 Java 配置或 YAML 配置来设置安全头部。
Java 配置
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.context.annotation.Bean;
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// HSTS
.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
.preload(true)
)
)
// CSP
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " +
"script-src 'self' 'strict-dynamic'; " +
"style-src 'self'; " +
"img-src 'self' data:; " +
"connect-src 'self' https://api.example.com; " +
"frame-src 'none'; " +
"frame-ancestors 'none'; " +
"report-uri https://example.com/csp-report"
)
)
)
// 其他安全头部
.headers(headers -> headers
.contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff
.frameOptions(frame -> frame.deny()) // X-Frame-Options: DENY
.referrerPolicy(referrer -> referrer // Referrer-Policy
.policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
.permissionsPolicy(permissions -> permissions // Permissions-Policy
.policy("camera=(), microphone=(), geolocation=()")
)
);
return http.build();
}
}application.yml 配置
server:
servlet:
session:
cookie:
http-only: true
secure: true
spring:
security:
headers:
hsts: max-age=31536000; includeSubDomains; preload
cache-control: no-cache, no-store, max-age=0, must-revalidate
x-content-type-options: nosniff
x-frame-options: DENY
content-security-policy: |
default-src 'self';
script-src 'self' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-src 'none';
frame-ancestors 'none';综合最佳实践总结
推荐的安全头部清单
以下是一份生产环境推荐的安全头部清单:
| 头部 | 推荐值 |
|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload |
Content-Security-Policy | 根据业务需要配置,推荐启用 nonce/strict-dynamic |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY(或通过 CSP 的 frame-ancestors 控制) |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | 禁用非必需 API,如 camera=(), microphone=(), geolocation=() |
分阶段实施策略
- 审计阶段:使用工具(如 securityheaders.com)扫描现有站点,了解当前安全头部配置状况。
- 报告阶段:对 CSP 等复杂策略先使用
Report-Only模式,收集违规报告,调整策略至无误报。 - 强制执行阶段:确认报告阶段无误后,切换为强制执行模式,并设置合理的
max-age值。 - 持续监控阶段:持续关注 CSP 报告和浏览器安全功能变更,及时更新策略。
常见风险提示
- 不要为了省事而使用
'unsafe-inline',它会极大削弱 CSP 的防护能力。 - 不要同时使用
X-Frame-Options和 CSPframe-ancestors时产生冲突。 - HSTS 的
includeSubDomains务必确认所有子域名均支持 HTTPS 后再启用。 - Permissions-Policy 的配置应遵循"最小权限原则",只开放业务必需的 API。
- Trusted Types 是一项强有力的防御机制,建议在新项目中优先考虑启用。