关于 security-headers
security-headers 是介绍通过中间件为 Web 应用配置浏览器安全响应头的开发者技能,涵盖 CSP、防点击劫持与 MIME 混淆、生产环境 HSTS,以及私有路由的索引控制。
主要功能
- 01Content-Security-Policy 配置
- 02使用 X-Frame-Options 防点击劫持
- 03使用 nosniff 防 MIME 混淆
- 04仅在生产环境启用 HSTS
- 05为仪表板路由设置禁止索引
适用场景
- 01在中间件中为 Web 请求添加响应头
- 02在 CSP 中允许所需的第三方域名
- 03检查部署后的安全响应头
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ harperaa/ secure-claude-skills --skill security-headers安装后在 Claude Code 中输入/security-headers确认已加载。
详细介绍
安全标头——防御多种攻击类型
为什么安全标头至关重要
可以把安全标头想象成城堡周围的城墙和护城河。即使攻击者突破了大门(你的身份验证),城墙(标头)也能阻止他们自由行动或窃取数据。
浏览器安全模型
现代浏览器内置了安全功能,但这些功能需要主动启用。如果没有正确的标头,浏览器会允许:
- 你的网站被嵌入恶意 iframe(点击劫持)
- 来自任何源的脚本(放大 XSS 攻击)
- 不安全的 HTTP 连接(中间人攻击)
- MIME 类型混淆(将图像作为脚本执行)
安全标头会告诉浏览器:“为我的网站启用所有安全功能。”
缺少标头的现实后果
根据 Scott Helme 对排名前 10,000 个网站进行的 2023 年安全审计,只有 2.8% 的网站正确实施了所有推荐的安全标头。其余 97.2% 的网站容易受到本可由标头防御的攻击。
Magecart 攻击(2018-2020 年): 数百个电商网站因注入了窃取支付信息的 JavaScript 而遭到入侵。Content-Security-Policy 标头本可以阻止这些脚本执行。缺少 CSP 的网站因欺诈交易损失了数百万美元。
我们的安全标头架构
每个请求都会通过 middleware.ts 自动应用所有标头。你无需手动设置——它们已经在保护你了。
我们应用的标头
- Content-Security-Policy(CSP)——控制资源加载
- X-Frame-Options: DENY——防止点击劫持
- X-Content-Type-Options: nosniff——防止 MIME 混淆
- Strict-Transport-Security(HSTS)——强制使用 HTTPS(仅限生产环境)
- X-Robots-Tag: noindex, nofollow——保护私有路由
标头说明与实现
1. Content-Security-Policy(CSP)
**作用:**控制可以加载哪些资源(脚本、样式、图像),以及这些资源可以来自哪里。
我们的配置:
// Dynamic CSP based on environment variables
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL
? new URL(process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL).origin
: '';
const convexDomain = process.env.NEXT_PUBLIC_CONVEX_URL
? new URL(process.env.NEXT_PUBLIC_CONVEX_URL).origin
: '';
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${clerkDomain} https://js.stripe.com`,
`style-src 'self' 'unsafe-inline' ${clerkDomain}`,
`connect-src 'self' ${clerkDomain} ${convexDomain} https://api.stripe.com`,
`frame-src 'self' ${clerkDomain} https://js.stripe.com https://hooks.stripe.com`,
"img-src 'self' data: https: blob:",
"font-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join('; ');
response.headers.set('Content-Security-Policy', csp);
这意味着:
- **脚本:**仅来自我们的域名、Clerk 和 Stripe
- **样式:**仅来自我们的域名和 Clerk
- **连接:**仅连接到 Clerk、Convex 和 Stripe API
- **框架:**仅允许 Clerk 和 Stripe
- **图像:**允许任何 HTTPS 来源(用于用户头像和外部图像)
为何采用动态配置:
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL
? new URL(process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL).origin
: ''
我们不会将 Clerk 的域名硬编码。该域名来自环境变量。这意味着:
- ✅ 开发、预发布和生产环境使用不同的域名(自动处理)
- ✅ 无需修改代码即可轻松更改
- ✅ 代码库中不包含安全密钥
**权衡:**我们允许脚本使用 unsafe-inline 和 unsafe-eval。这是 Next.js 和 Clerk 正常运行所必需的。我们通过输入清理和验证来降低这一风险。
防止数据外泄:
connect-src 指令可阻止恶意脚本向未经授权的域名发送数据。即使 XSS 绕过了清理,浏览器仍会拦截未经授权的网络请求。
2. X-Frame-Options: DENY
**防范对象:**点击劫持攻击,即攻击者将你的网站嵌入其网站上的不可见 iframe 中。
攻击场景: 攻击者将你的“删除账户”按钮嵌入游戏网站上的不可见 iframe 覆盖层中。用户以为自己点击的是“开始游戏”,实际点击的却是“删除账户”。
我们的防护:
response.headers.set('X-Frame-Options', 'DENY');
DENY 表示浏览器会拒绝将我们的网站嵌入任何 iframe,即使是我们自己的网站也不例外。提供最高级别的安全性。
其他可选值:
DENY——完全禁止 iframe(最安全,我们采用此设置)SAMEORIGIN——仅允许我们自己的网站通过 iframe 嵌入ALLOW-FROM uri——已弃用,请勿使用
3. X-Content-Type-Options: nosniff
防护目标: 浏览器将图片误判为 JavaScript 并执行的 MIME 混淆攻击。
攻击场景: 攻击者上传了一个包含 JavaScript 的文件「avatar.jpg」。旧版浏览器试图“帮忙”,会“嗅探”文件类型,检测到 JavaScript 后执行它。
我们的防护:
response.headers.set('X-Content-Type-Options', 'nosniff');
nosniff 告诉浏览器严格遵循 Content-Type 标头,绝不自行猜测。
为何重要: 如果没有此标头,攻击者可以:
- 上传包含
<script>evil()</script>的「image.jpg」 - 链接到它:
<img src="/uploads/image.jpg"> - 浏览器嗅探文件,检测到脚本并执行
- 尽管上传验证已通过,仍然实现 XSS 攻击
启用 nosniff 后,浏览器会看到 Content-Type: image/jpeg,并拒绝将其作为脚本执行。
4. Strict-Transport-Security (HSTS) - 仅限生产环境
防护目标: 中间人降级 HTTPS 为 HTTP 的 SSL 剥离攻击。
攻击场景: 用户输入「yourapp.com」(没有 https://)。浏览器最初会请求 HTTP。攻击者拦截请求,提供伪造的 HTTP 版本并窃取凭据。
我们的防护:
if (process.env.NODE_ENV === 'production') {
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
配置说明:
max-age=31536000- 有效期为 1 年includeSubDomains- 适用于所有子域名- 设置后,浏览器只会对你的域名使用 HTTPS
为何仅限生产环境: 在开发环境中,你使用的是 localhost(HTTP)。HSTS 会导致本地开发无法进行。我们的中间件会检测环境,并仅在生产环境启用 HSTS。
重要提示: 为域名设置 HSTS 后,浏览器会记住此设置。如果需要移除,必须:
- 设置
max-age=0 - 等待所有用户的浏览器收到新的标头
- 如果 max-age 较长,这可能需要数月
5. X-Robots-Tag: noindex, nofollow(受保护的路由)
防护目标: 防止搜索引擎为私密内容编制索引。
为何重要:
你不会希望 /dashboard/payment-details 出现在 Google 搜索结果中。
我们的实现:
if (req.nextUrl.pathname.startsWith('/dashboard')) {
response.headers.set('X-Robots-Tag', 'noindex, nofollow');
}
应用范围: 仅适用于 /dashboard/* 路由(公共页面应编入索引)
此设置告知搜索引擎:
noindex- 不要将此页面添加到搜索结果nofollow- 不要跟踪此页面上的链接
在中间件中实现
middleware.ts 模式
import { clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export default clerkMiddleware((auth, req) => {
const response = NextResponse.next();
// Get dynamic domains from environment
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL
? new URL(process.env.NEXT_PUBLIC_CLERK_FRONTEND_API_URL).origin
: '';
const convexDomain = process.env.NEXT_PUBLIC_CONVEX_URL
? new URL(process.env.NEXT_PUBLIC_CONVEX_URL).origin
: '';
// Build CSP dynamically
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${clerkDomain} https://js.stripe.com`,
`style-src 'self' 'unsafe-inline' ${clerkDomain}`,
`connect-src 'self' ${clerkDomain} ${convexDomain} https://api.stripe.com`,
`frame-src 'self' ${clerkDomain} https://js.stripe.com https://hooks.stripe.com`,
"img-src 'self' data: https: blob:",
"font-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join('; ');
// Apply security headers
response.headers.set('Content-Security-Policy', csp);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
// HSTS only in production
if (process.env.NODE_ENV === 'production') {
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
// Prevent indexing of protected routes
if (req.nextUrl.pathname.startsWith('/dashboard')) {
response.headers.set('X-Robots-Tag', 'noindex, nofollow');
}
return response;
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
测试安全标头
测试所有标头
curl -I http://localhost:3000
# Expected headers:
# X-Frame-Options: DENY
# X-Content-Type-Options: nosniff
# Content-Security-Policy: default-src 'self'; ...
测试生产环境 HSTS
# In production
curl -I https://yourapp.com
# Should include:
# Strict-Transport-Security: max-age=31536000; includeSubDomains
测试受保护路由的标头
curl -I http://localhost:3000/dashboard
# Should include:
# X-Robots-Tag: noindex, nofollow
在线标头测试
使用以下在线工具测试已部署的网站:
- Security Headers: https://securityheaders.com/
- Mozilla Observatory: https://observatory.mozilla.org/
- SSL Labs: https://www.ssllabs.com/ssltest/
为新的集成自定义 CSP
添加新的第三方服务时:
示例:添加 Google Analytics
// middleware.ts
// Add Google Analytics domain to CSP
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${clerkDomain} https://js.stripe.com https://www.googletagmanager.com https://www.google-analytics.com`,
`style-src 'self' 'unsafe-inline' ${clerkDomain}`,
`connect-src 'self' ${clerkDomain} ${convexDomain} https://api.stripe.com https://www.google-analytics.com https://analytics.google.com`,
// ... rest of CSP
].join('; ');
示例:添加自定义 CDN
const cdnDomain = process.env.NEXT_PUBLIC_CDN_URL
? new URL(process.env.NEXT_PUBLIC_CDN_URL).origin
: '';
const csp = [
"default-src 'self'",
`script-src 'self' ${cdnDomain}`,
`style-src 'self' ${cdnDomain}`,
`img-src 'self' ${cdnDomain} https:`,
// ... rest of CSP
].join('; ');
常见 CSP 问题及解决方案
问题 1:内联脚本被阻止
症状: <script> 标签中的脚本无法执行
解决方案: Next.js 需要 unsafe-inline 和 unsafe-eval。我们的 CSP 已配置这两项。
更好的替代方案: 使用 nonce(需要修改 SSR):
const nonce = crypto.randomBytes(16).toString('base64');
script-src 'self' 'nonce-${nonce}'
问题 2:外部图片无法加载
症状: Gravatar 等来源的用户头像无法显示
当前解决方案: img-src 'self' data: https: blob: 允许所有 HTTPS 图片
更严格的替代方案:
`img-src 'self' data: https://gravatar.com https://images.yourapp.com`
问题 3:WebSocket 连接失败
症状: Convex 实时更新无法正常工作
解决方案: 确保 connect-src 包含 Convex 域名:
`connect-src 'self' ${convexDomain}`
安全标头可防护的攻击
✅ 点击劫持(X-Frame-Options) ✅ XSS 放大攻击(CSP) ✅ MIME 混淆(X-Content-Type-Options) ✅ SSL 剥离(HSTS) ✅ 私有数据暴露给搜索引擎(X-Robots-Tag) ✅ 数据外泄(CSP connect-src) ✅ 未授权资源加载(CSP default-src)
常见错误,应避免
❌ 不要在 CSP 中硬编码域名 — 使用环境变量
❌ 不要在开发环境中启用 HSTS — 会导致 localhost 无法使用
❌ 不要使用 X-Frame-Options: ALLOW-FROM — 已弃用
❌ 不要忘记在部署后测试标头
❌ 不要设置过于宽松的 CSP(例如使用 * 通配符)
✅ 使用环境变量配置动态 CSP ✅ 在部署后使用在线工具测试标头 ✅ 添加新的第三方服务时更新 CSP ✅ 仅在生产环境中启用 HSTS ✅ 保护仪表板路由,避免被编入索引
参考资料
- Mozilla 安全标头指南:https://infosec.mozilla.org/guidelines/web_security
- OWASP 安全标头项目:https://owasp.org/www-project-secure-headers/
- 内容安全策略:https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- HSTS 规范:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
- 安全标头检查器:https://securityheaders.com/
后续步骤
- 如需防止 XSS:使用
input-validationskill - 如需进行点击劫持测试:使用
security-testingskill - 标头会自动设置 — 查看
middleware.ts:1了解实现方式 - 如需添加新的集成:在 middleware.ts 中更新 CSP