Helmet
@vafast/helmet 为响应附加常见安全相关 HTTP 头(CSP、HSTS、X-Frame-Options 等)。只改响应头,不改业务逻辑。
调用 vafastHelmet(config?) 返回中间件:在 next() 拿到业务响应后,把安全头合并进新的 Response 再返回。
先搞清几个概念(给新用户)
浏览器会按响应头决定「能不能嵌 iframe」「脚本从哪加载」「是否强制 HTTPS」等。Helmet 就是帮你批量写好这些头,减少漏配。
| 名词 | 白话 |
|---|---|
| CSP(Content-Security-Policy) | 告诉浏览器:脚本、样式、图片、接口请求等允许从哪些来源加载。收紧后可明显降低 XSS 注入脚本的危害 |
| Nonce | 一次性随机串。开启 useNonce 后写入 CSP,并额外返回 X-Nonce。页面里内联 <script> / <style> 需带上同一 nonce,才允许执行 |
| HSTS(Strict-Transport-Security) | 告诉浏览器:以后访问本站必须用 HTTPS,并记住一段时间。本包仅在 NODE_ENV === 'production' 时写出,避免本地 HTTP 开发被「锁死」 |
| X-Frame-Options | 控制本站能否被别的页面用 <iframe> 嵌入,用来防点击劫持 |
| Referrer-Policy | 控制跳转到外站时,浏览器会不会带上完整来源 URL(可能含路径、查询串) |
| Permissions-Policy | 控制页面能否用摄像头、麦克风、定位等浏览器能力;空数组 [] 表示禁用该能力 |
| CORP / COOP | 跨源资源策略 / 跨源打开者策略:限制谁能加载你的资源、弹窗之间是否共享浏览上下文(与隔离、安全相关) |
| Report-To | 告诉浏览器把违规/崩溃等报告发到哪些端点(配合监控) |
不必一次全懂:默认配置已经可用;业务有 CDN、第三方脚本时再改 CSP。
安装
npm install @vafast/helmet快速开始
import { Server, defineRoute, defineRoutes, json, serve } from 'vafast'
import { vafastHelmet } from '@vafast/helmet'
const routes = defineRoutes([
defineRoute({
method: 'GET',
path: '/',
handler: () => json({ ok: true }),
}),
])
const server = new Server(routes)
server.use(vafastHelmet())
serve({ fetch: server.fetch, port: 3000 })零配置时会带上默认 CSP、X-Frame-Options: DENY、XSS 相关头、Referrer-Policy、Permissions-Policy、CORP/COOP 等;HSTS 仅在生产环境出现。另会始终设置 X-Content-Type-Options: nosniff(不可通过选项关闭)。
用法
全局挂载
server.use(vafastHelmet())单路由挂载
defineRoute({
method: 'GET',
path: '/secure',
middleware: [vafastHelmet()],
handler: () => json({ ok: true }),
})自定义 CSP
传入的 csp 会与默认 CSP 浅合并(只覆盖你写的字段,其余保留默认):
import { vafastHelmet, permission } from '@vafast/helmet'
server.use(
vafastHelmet({
csp: {
defaultSrc: [permission.SELF],
scriptSrc: [permission.SELF],
imgSrc: [permission.SELF, permission.DATA, 'https:'],
},
frameOptions: 'SAMEORIGIN',
}),
)开启 CSP Nonce
csp.useNonce: true 时会生成 nonce,写入 script-src / style-src,并额外设置响应头 X-Nonce(业务侧把该值注入 HTML 模板):
server.use(
vafastHelmet({
csp: {
useNonce: true,
scriptSrc: [permission.SELF],
styleSrc: [permission.SELF],
},
}),
)Report-Only 模式
先观察违规、不拦截加载,适合上线前试跑:
server.use(
vafastHelmet({
csp: {
reportOnly: true,
reportUri: '/csp-report',
},
}),
)此时写出的是 Content-Security-Policy-Report-Only,而不是正式的 Content-Security-Policy。
配置 Report-To
server.use(
vafastHelmet({
reportTo: [
{
group: 'csp-endpoint',
maxAge: 10886400,
endpoints: [{ url: 'https://example.com/reports' }],
includeSubdomains: true,
},
],
}),
)额外自定义头
server.use(
vafastHelmet({
customHeaders: {
'X-App-Version': '1.0.0',
},
}),
)API
导出
| 导出 | 说明 |
|---|---|
vafastHelmet(config?) | 主入口,返回中间件 |
elysiaHelmet | vafastHelmet 的兼容别名 |
permission | CSP 常用字面量常量(见下表) |
SecurityConfig | 顶层配置类型 |
CSPConfig | CSP 子配置类型 |
HSTSConfig | HSTS 子配置类型 |
ReportToConfig | Report-To 单项配置类型 |
vafastHelmet(config?: Partial<SecurityConfig>)
未传的字段使用默认值;csp / hsts / permissionsPolicy 在传入时与默认对象浅合并。
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
csp | CSPConfig | 见下方「默认 CSP」与「CSP 指令含义」 | Content-Security-Policy(或 Report-Only 变体) |
frameOptions | 'DENY' | 'SAMEORIGIN' | 'ALLOW-FROM' | 'DENY' | X-Frame-Options。DENY = 禁止任何嵌入;SAMEORIGIN = 仅同源可嵌;ALLOW-FROM 为历史取值,现代浏览器支持差,一般不要用 |
xssProtection | boolean | true | 为 true 时写 X-XSS-Protection: 1; mode=block(旧浏览器遗留机制;现代防护仍主要靠 CSP) |
dnsPrefetch | boolean | false | X-DNS-Prefetch-Control:true → on,false → off |
referrerPolicy | 见下方枚举 | 'strict-origin-when-cross-origin' | Referrer-Policy:控制外链请求携带多少 Referer |
permissionsPolicy | Record<string, string[]> | 见下方「默认 Permissions-Policy」 | Permissions-Policy;某能力对应空数组表示禁用 |
hsts | HSTSConfig | { maxAge: 15552000, includeSubDomains: true, preload: true } | 仅 NODE_ENV === 'production' 时写入 Strict-Transport-Security |
corp | 'same-origin' | 'same-site' | 'cross-origin' | 'same-origin' | Cross-Origin-Resource-Policy:谁可以加载本响应为资源 |
coop | 'unsafe-none' | 'same-origin-allow-popups' | 'same-origin' | 'same-origin' | Cross-Origin-Opener-Policy:与弹窗 / window.opener 隔离相关 |
reportTo | ReportToConfig[] | — | Report-To 头(JSON);未配置则不写 |
customHeaders | Record<string, string> | — | 额外自定义响应头,直接合并 |
另会始终设置 X-Content-Type-Options: nosniff(防止浏览器把非脚本 MIME 误当成脚本执行)。
referrerPolicy 可选值
| 值 | 白话 |
|---|---|
no-referrer | 从不发送 Referer |
no-referrer-when-downgrade | HTTPS→HTTP 时不发,其它情况发完整 URL |
origin | 只发源(协议+主机+端口) |
origin-when-cross-origin | 同源发完整 URL,跨源只发 origin |
same-origin | 仅同源请求带 Referer |
strict-origin | 只发 origin;HTTPS→HTTP 不发 |
strict-origin-when-cross-origin(默认) | 同源完整 URL;跨源只发 origin;降级不发 |
unsafe-url | 总是发完整 URL(可能泄露路径/查询串,慎用) |
默认 CSP
| 指令(配置字段) | 默认值 |
|---|---|
defaultSrc | [permission.SELF]('self') |
scriptSrc | [permission.SELF, permission.UNSAFE_INLINE] |
styleSrc | [permission.SELF, permission.UNSAFE_INLINE] |
imgSrc | [permission.SELF, permission.DATA, permission.BLOB] |
fontSrc | [permission.SELF] |
connectSrc | [permission.SELF] |
frameSrc | [permission.SELF] |
objectSrc | [permission.NONE] |
baseUri | [permission.SELF] |
默认含 'unsafe-inline',是为了让未改造的内联脚本/样式先能跑;生产建议逐步去掉,改用 nonce 或外链。
CSP 指令含义(CSPConfig)
配置字段为 camelCase,写出的头里会转成 kebab-case(如 scriptSrc → script-src)。数组里每一项是一个允许的源表达式。
| 字段 | 对应指令 | 白话 |
|---|---|---|
defaultSrc | default-src | 兜底:其它未单独声明的资源类型走这里 |
scriptSrc | script-src | 允许执行哪些脚本来源 |
styleSrc | style-src | 允许加载哪些样式来源 |
imgSrc | img-src | 允许加载哪些图片来源 |
fontSrc | font-src | 允许加载哪些字体来源 |
connectSrc | connect-src | 允许 fetch / XHR / WebSocket 等连哪些源 |
frameSrc | frame-src | 允许本页嵌哪些源的 frame / iframe |
objectSrc | object-src | 允许 <object> / <embed> / <applet> 的源;默认 'none' |
baseUri | base-uri | 限制 <base href> 能设到哪,防被改基准 URL |
reportUri | report-uri | CSP 违规报告提交地址(字符串,不是数组) |
useNonce | —(布尔开关) | 为 true 时给 script-src / style-src 追加 'nonce-...',并写响应头 X-Nonce |
reportOnly | —(布尔开关) | 为 true 时使用 Content-Security-Policy-Report-Only,只上报不拦截 |
useNonce / reportOnly 不会被拼进 CSP 指令字符串,只影响生成逻辑。
HSTSConfig
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
maxAge | number | 15552000(约 180 天) | 浏览器记住「必须 HTTPS」的秒数。初始化时若 < 0 会抛错 |
includeSubDomains | boolean | true | 为 true 时附加 ; includeSubDomains,子域也强制 HTTPS |
preload | boolean | true | 为 true 时附加 ; preload(提交浏览器预加载列表时需要;未全站 HTTPS 勿开) |
再次强调:即使配置了 hsts,非 production 也不会写出 Strict-Transport-Security。
默认 Permissions-Policy
| 能力键 | 默认 | 白话 |
|---|---|---|
camera | [] | 禁用摄像头 |
microphone | [] | 禁用麦克风 |
geolocation | [] | 禁用地理位置 |
interest-cohort | [] | 禁用 FLoC / interest-cohort 类追踪相关能力 |
空数组序列化为 camera=() 这种「不允许任何源」的形式。若要允许自身:{ camera: ["self"] }(键名按浏览器 Permissions-Policy 规范书写)。
传入 permissionsPolicy 时与默认表浅合并(只覆盖你写的键)。
ReportToConfig
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
group | string | 是 | 端点组名 |
maxAge | number | 是 | 配置缓存秒数;< 0 时初始化抛错 |
endpoints | Array<{ url: string; priority?: number; weight?: number }> | 是 | 至少一个端点,否则初始化抛错 |
includeSubdomains | boolean | 否 | 是否包含子域 |
写出时字段名会转为规范 JSON:max_age、include_subdomains。
permission 常量
写 CSP 数组时优先用常量,避免漏引号:
| 常量 | 值 | 白话 |
|---|---|---|
permission.SELF | 'self' | 允许与当前文档同源 |
permission.UNSAFE_INLINE | 'unsafe-inline' | 允许内联脚本/样式(削弱 XSS 防护,尽量少用) |
permission.HTTPS | https: | 允许任意 HTTPS 源(很宽) |
permission.DATA | data: | 允许 data: URL(常见于小图) |
permission.NONE | 'none' | 不允许任何源 |
permission.BLOB | blob: | 允许 blob: URL |
也可在数组里直接写具体源,如 'https://cdn.example.com'。
最佳实践
- 生产环境再依赖 HSTS;本地开发不要强行模拟
NODE_ENV=production,除非已全站 HTTPS。 - CSP 按业务收紧:尽量去掉
'unsafe-inline',需要内联脚本时用useNonce。 - 对外嵌入第三方页面时,按需放宽
frameOptions/corp/coop,避免误伤合法跨域。 - 新策略可先用
csp.reportOnly: true收集违规,再切正式拦截。 - 使用 CDN / 第三方埋点时,把对应域名加进
scriptSrc/connectSrc/imgSrc,否则会被浏览器拦掉。
注意事项
- HSTS 只在生产环境写出;非 production 即使配置了
hsts也不会设置该头。 hsts.maxAge < 0或reportTo配置非法(maxAge < 0、endpoints 为空)会在初始化时抛错。- 中间件在
next()之后包装Response写入头;下游若返回不可变 Headers,行为取决于运行时。 frameOptions: 'ALLOW-FROM'已过时,现代浏览器请优先用 CSP 的frame-ancestors(本包 CSP 配置未单独暴露该指令字段;若需要可考虑customHeaders或收紧其它策略)。- Nonce 每次请求重新生成;SSR 页面必须把当次响应的
X-Nonce注入到 HTML,不能写死。