mcpField
S
security-headers

security-headers

github.com/harperaa安全19202 次安装45 周前更新
代码仓库

关于 security-headers

security-headers 是介绍通过中间件为 Web 应用配置浏览器安全响应头的开发者技能,涵盖 CSP、防点击劫持与 MIME 混淆、生产环境 HSTS,以及私有路由的索引控制。

主要功能

  1. 01Content-Security-Policy 配置
  2. 02使用 X-Frame-Options 防点击劫持
  3. 03使用 nosniff 防 MIME 混淆
  4. 04仅在生产环境启用 HSTS
  5. 05为仪表板路由设置禁止索引

适用场景

  1. 01在中间件中为 Web 请求添加响应头
  2. 02在 CSP 中允许所需的第三方域名
  3. 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 自动应用所有标头。你无需手动设置——它们已经在保护你了。

我们应用的标头
  1. Content-Security-Policy(CSP)——控制资源加载
  2. X-Frame-Options: DENY——防止点击劫持
  3. X-Content-Type-Options: nosniff——防止 MIME 混淆
  4. Strict-Transport-Security(HSTS)——强制使用 HTTPS(仅限生产环境)
  5. 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 标头,绝不自行猜测。

为何重要: 如果没有此标头,攻击者可以:

  1. 上传包含 <script>evil()</script> 的「image.jpg」
  2. 链接到它:<img src="/uploads/image.jpg">
  3. 浏览器嗅探文件,检测到脚本并执行
  4. 尽管上传验证已通过,仍然实现 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 后,浏览器会记住此设置。如果需要移除,必须:

  1. 设置 max-age=0
  2. 等待所有用户的浏览器收到新的标头
  3. 如果 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
在线标头测试

使用以下在线工具测试已部署的网站:

为新的集成自定义 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 ✅ 保护仪表板路由,避免被编入索引

参考资料

后续步骤

  • 如需防止 XSS:使用 input-validation skill
  • 如需进行点击劫持测试:使用 security-testing skill
  • 标头会自动设置 — 查看 middleware.ts:1 了解实现方式
  • 如需添加新的集成:在 middleware.ts 中更新 CSP

评论

支持 Markdown