Secure Headers 安全头

要点

  • secureHeaders() 一键设置常见的安全响应头,减少 XSS、点击劫持、MIME 嗅探等风险
  • 默认配置覆盖了大部分生产场景,开箱即用
  • CSP(Content Security Policy)是最复杂但也最有效的安全头,控制页面能加载哪些资源
  • permissionsPolicy 控制浏览器 API 的访问权限(摄像头、麦克风、地理位置等)
  • 安全头不能解决所有安全问题,它们是纵深防御的一层
  • 配置错误会导致页面功能异常,上线前需要在 staging 环境充分测试

1. 基本用法

secureHeaders() 中间件设置一组常见的安全响应头:

// src/index.ts
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
 
const app = new Hono()
 
app.use('*', secureHeaders())
 
app.get('/', (c) => c.html('<html><body>Hello</body></html>'))
 
export default app

响应头:

X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

每个头的含义:

  • X-Frame-Options: SAMEORIGIN:只允许同源域名用 &lt;iframe&gt; 嵌套页面,防止点击劫持
  • X-Content-Type-Options: nosniff:禁止浏览器猜测 Content-Type,防止 MIME 嗅探攻击
  • X-XSS-Protection: 0:禁用浏览器内置 XSS 过滤器(现代浏览器已废弃这个机制,设为 0 避免副作用)
  • Referrer-Policy: strict-origin-when-cross-origin:跨域请求只发送 origin 作为 Referer,不暴露完整 URL
  • Permissions-Policy:禁用摄像头、麦克风、地理位置等浏览器 API

默认配置适合大部分场景。如果项目有特殊需求,可以覆盖默认值。

2. 覆盖默认配置

secureHeaders() 接收配置对象,可以覆盖默认值:

// src/index.ts
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
 
const app = new Hono()
 
app.use('*', secureHeaders({
  xFrameOptions: { action: 'ALLOW-FROM', origin: 'https://trusted.com' },
  xContentTypeOptions: false,  // 禁用某个头
  referrerPolicy: { policy: 'no-referrer' },
}))
 
export default app

配置项与默认值:

{
  xFrameOptions: { action: 'SAMEORIGIN' },   // DENY | SAMEORIGIN | ALLOW-FROM
  xContentTypeOptions: true,                  // nosniff
  xXssProtection: false,                      // 0(禁用)
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  permissionsPolicy: {
    camera: [],
    microphone: [],
    geolocation: [],
    // ...
  },
}

action: 'DENY' 完全禁止 iframe 嵌套,比 SAMEORIGIN 更严格。如果页面不需要被任何域名嵌套,用 DENY

3. Content Security Policy

CSP 是最复杂但也最有效的安全头。它告诉浏览器页面只能加载指定来源的资源,防止 XSS、数据注入等攻击。

默认 secureHeaders() 不设置 CSP,因为 CSP 配置与项目内容强相关,没有通用默认值。

手动添加 CSP:

// src/index.ts
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
 
const app = new Hono()
 
app.use('*', secureHeaders({
  contentSecurityPolicy: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'", 'https://analytics.example.com'],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'", 'https://api.example.com'],
    fontSrc: ["'self'", 'data:'],
    objectSrc: ["'none'"],
    frameSrc: ["'none'"],
    upgradeInsecureRequests: true,
  },
}))
 
export default app

响应头:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://analytics.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' data:; object-src 'none'; frame-src 'none'; upgrade-insecure-requests

每个指令的含义:

  • default-src:其他指令的默认值
  • scriptSrc:允许的 JavaScript 来源
  • styleSrc:允许的 CSS 来源
  • imgSrc:允许的图片来源
  • connectSrc:允许的 fetch、WebSocket 来源
  • fontSrc:允许的字体来源
  • objectSrc:允许的 &lt;object&gt;&lt;embed&gt; 来源
  • frameSrc:允许的 iframe 来源
  • upgradeInsecureRequests:自动把 HTTP 请求升级为 HTTPS

'self' 表示同源,'unsafe-inline' 允许内联脚本/样式。使用 'unsafe-inline' 会降低 CSP 的保护效果,但很多项目依赖内联样式(例如 CSS-in-JS),需要权衡。

4. CSP 的调试

CSP 配置错误会导致页面功能异常——脚本被拦截、样式丢失、图片加载失败。调试方式:

4.1 Report-Only 模式

先用 Content-Security-Policy-Report-Only 代替 Content-Security-Policy,浏览器会报告违规但不阻止资源加载:

app.use('*', secureHeaders({
  contentSecurityPolicy: {
    // ... 配置
    reportOnly: true,  // 只报告,不阻止
  },
}))

浏览器控制台的违规报告格式:

Refused to load the script 'https://evil.com/script.js' because it violates the following Content Security Policy directive: 'script-src 'self''

4.2 配置 report-uri 或 report-to

把违规报告发送到指定的 URL:

contentSecurityPolicy: {
  // ... 配置
  reportUri: '/csp-violations',
  // 或
  reportTo: 'csp-endpoint',
}
// 接收违规报告的端点
app.post('/csp-violations', async (c) => {
  const report = await c.req.json()
  console.log('CSP violation:', report)
  return c.json({ ok: true })
})

4.3 nonce 替代 unsafe-inline

如果项目使用了 CSP 但不想使用 'unsafe-inline',可以用 nonce:

import { randomBytes } from 'crypto'
 
app.use('*', async (c, next) => {
  const nonce = randomBytes(16).toString('base64')
  c.set('cspNonce', nonce)
  c.header('Content-Security-Policy', `script-src 'self' 'nonce-${nonce}'`)
  await next()
})
 
app.get('/', (c) => {
  const nonce = c.get('cspNonce') as string
  return c.html(`
    <html>
      <head>
        <script nonce="${nonce}">
          console.log('This script is allowed')
        </script>
      </head>
      <body>Hello</body>
    </html>
  `)
})

每次请求生成不同的 nonce,内联脚本必须携带匹配的 nonce 才能执行。这样即使攻击者注入了内联脚本,也无法通过 CSP 校验。

5. Permissions Policy

Permissions-Policy 控制页面可以使用哪些浏览器 API:

app.use('*', secureHeaders({
  permissionsPolicy: {
    camera: [],                    // 禁止使用摄像头
    microphone: [],                // 禁止使用麦克风
    geolocation: ['https://maps.example.com'],  // 只允许指定域名使用地理位置
    payment: [],                   // 禁止使用 Payment API
    usb: [],                       // 禁止访问 USB 设备
  },
}))

空的数组 [] 表示禁止所有来源(包括同源)。如果允许同源,使用 'self'

permissionsPolicy: {
  camera: ['self'],  // 只允许同源使用摄像头
}

Permissions-Policy 对纯 API 服务意义不大(API 响应不涉及浏览器 UI),但对提供 HTML 页面的服务很重要。

6. 安全头的局限

安全头是纵深防御的一层,不能解决所有安全问题:

  • CSP 防止 XSS,但不能防止服务端漏洞(SQL 注入、命令注入)
  • X-Frame-Options 防止点击劫持,但不能防止 CSRF
  • HSTS(Strict-Transport-Security)强制 HTTPS,但 secureHeaders() 默认不设置

HSTS 需要显式添加:

app.use('*', async (c, next) => {
  c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
  await next()
})

HSTS 告诉浏览器在指定时间内(这里是 1 年)只能通过 HTTPS 访问该域名。配置错误可能导致 HTTP 用户无法访问,上线前要确认 HTTPS 配置稳定。

7. 不同路由不同策略

有些安全头只对 HTML 页面有意义,API 响应不需要。可以按路径挂载:

// src/index.ts
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
 
const app = new Hono()
 
// HTML 页面:完整的安全头配置
app.use('/app/*', secureHeaders({
  contentSecurityPolicy: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    // ...
  },
}))
 
// API 响应:只需要基础安全头
app.use('/api/*', secureHeaders({
  contentSecurityPolicy: false,  // API 不需要 CSP
  xFrameOptions: { action: 'DENY' },
}))
 
export default app

API 不需要 CSP(没有 HTML 可执行),也不需要 Permissions-Policy。但 X-Content-Type-Options、Referrer-Policy 对所有响应都有意义。

8. 测试安全头

安全头配置是否正确,可以通过响应头直接验证:

// tests/secure-headers.test.ts
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
 
describe('secure headers', () => {
  it('设置基础安全头', async () => {
    const app = new Hono()
    app.use('*', secureHeaders())
    app.get('/', (c) => c.text('ok'))
 
    const res = await app.request('/')
 
    expect(res.headers.get('X-Frame-Options')).toBe('SAMEORIGIN')
    expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff')
    expect(res.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin')
  })
 
  it('设置 CSP', async () => {
    const app = new Hono()
    app.use('*', secureHeaders({
      contentSecurityPolicy: {
        defaultSrc: ["'self'"],
      },
    }))
    app.get('/', (c) => c.html('<html></html>'))
 
    const res = await app.request('/')
    expect(res.headers.get('Content-Security-Policy')).toContain("default-src 'self'")
  })
})

也可以用在线工具(例如 securityheaders.com)扫描生产环境的响应头,检查配置是否完整。

总结

secureHeaders() 中间件一键设置常见的安全响应头,减少 XSS、点击劫持、MIME 嗅探等风险。默认配置覆盖了大部分生产场景。

这一节涉及到的几个层次:

  1. 默认安全头:X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy
  2. 覆盖默认值:通过配置对象调整各个头的行为
  3. CSP:最复杂但最有效的安全头,控制页面资源加载来源
  4. CSP 调试:Report-Only 模式、report-uri、nonce 替代 unsafe-inline
  5. Permissions-Policy:控制浏览器 API 访问权限
  6. 安全头的局限:不能替代其他安全措施,HSTS 需要单独配置
  7. 按路径差异化:HTML 页面和 API 响应可以配置不同的安全策略

安全头的配置错误会导致页面功能异常。上线前需要在 staging 环境充分测试,尤其是 CSP。

下一篇看 Rate Limit 限流中间件——限流策略、存储后端、自定义 key 生成。