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:只允许同源域名用
<iframe>嵌套页面,防止点击劫持 - 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:允许的
<object>、<embed>来源 - 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 appAPI 不需要 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 嗅探等风险。默认配置覆盖了大部分生产场景。
这一节涉及到的几个层次:
- 默认安全头:X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy
- 覆盖默认值:通过配置对象调整各个头的行为
- CSP:最复杂但最有效的安全头,控制页面资源加载来源
- CSP 调试:Report-Only 模式、report-uri、nonce 替代 unsafe-inline
- Permissions-Policy:控制浏览器 API 访问权限
- 安全头的局限:不能替代其他安全措施,HSTS 需要单独配置
- 按路径差异化:HTML 页面和 API 响应可以配置不同的安全策略
安全头的配置错误会导致页面功能异常。上线前需要在 staging 环境充分测试,尤其是 CSP。
下一篇看 Rate Limit 限流中间件——限流策略、存储后端、自定义 key 生成。