👹 Helmet & Security
Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it can help!1
Helmet 是一个 Express 中间件,它更改了 HTTP 请求的某些响应头,以告知浏览器使用某种安全策略。Helmet 不能带来绝对的安全,比如针对 DNS Rebinding2 问题,它就无能为力3。不过尽管不是银弹,它确实还是很有效的。
仅需数行代码,就可以引入 Helmet 并使用:
const express = require('express')
const app = express()
app.use(require('helmet')())
新的响应请求的 Headers 会变成这样:
HTTP/1.1 200 OK
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
...
那么,接收到此响应头之后,浏览器具体会执行哪些操作呢?
11 种安全策略
Helmet 使用了共 11 种安全策略。其中相关的许多 HTTP 头都源于 OWASP Secure Headers
4 这个项目。以下我们从简单的请求头开始,看看不同策略的背后相关的安全问题。
移除 X-Powered-By
response.removeHeader('X-Powered-By')
一些框架会在请求头中添加 X-Powered-By
(或 Server) 以标明网站是由哪种框架搭建的,这对宣传框架做了推力。但泄漏框架的版本信息可能带来的问题是,攻击者可以寻找针对该版本的已知漏洞轻松发起攻击。
由于 Express 默认会给响应头增加 X-Powered-By
,所以 Helmet 直接把它移除了。
移除 MIME 嗅探
response.setHeader('X-Content-Type-Options', 'nosniff')
浏览器的 MIME 嗅探是指某些浏览器(如 IE8)会根据文件内容,而不是 Content-Type
,执行、渲染文件。这可能使攻击者发送的图片等文件中嵌套的脚本代码得到执行。解决方案也很简单,只需用响应头 X-Content-Type-Options = nosniff
告诉浏览器把 MIME 嗅探关闭就完事儿了。
移除 X-XSS-Protection
response.setHeader('X-XSS-Protection', '0')
不是应该打开 X-XSS-Protection
,以防范 XSS 攻击么,为什么要把它关闭?
这得追溯回去年的 Chrome 移除 XSS Auditor
,准备使用新的 XSS 防护方法这事儿上。“XSS Auditor 已经充满了漏洞”,并且“修复所有信息泄漏已经证明是困难的”。所以就把这玩意儿给废除了5。至于程序员们最担心的 Edge,它已经在 18 年去除了 XSS Auditor
,并开始使用 CSP
等现代标准(更现代的标准)6。也就是说,应用程序应该尽可能跟着新标准走,而 X-XSS-Protection
这个响应头是让浏览器不要再用 XSS Auditor
。
见:#376。
禁止页面被嵌套
response.setHeader('X-Frame-Options', 'SAMEORIGIN')
X-Frame-Options
指定了浏览器的 frame、iframe、object、embed 等元素的有效父级作用域。限制 X-Frame-Options
为 SameOrigin
,可以防止网页被 iframe 等元素嵌套到非同源页面中,预防某些点击劫持攻击。
本来它的值有三种选择,Deny
、SameOrigin
、AllowFrom
。但 AllowFrom
因为其兼容性原因,被 Helmet 弃用:
此外,可以在 CSP
中设置 frame-ancestors
指令来替代 X-Frame-Options
。
移除 Referer
Referrer-Policy
可以控制浏览器请求头的 Referer
7 的显示方式。移除 Referer
能保护用户的访问记录,防止隐私泄漏。
const ALLOWED_TOKENS = new Set([
// 任何时候都不携带 Referer
'no-referrer',
// 仅当协议降级时不携带 Referer(浏览器默认策略)
'no-referrer-when-downgrade',
// 同源请求携带 Referer,非同源请求不携带 Referer
'same-origin',
// 所有请求,Referer 都指向来源的源地址
'origin',
// 在 origin 的基础上,当协议降级时,不携带 Referer
'strict-origin',
// 同源请求时发送完整的 Rerferer,非同源请求只发送来源的源地址
'origin-when-cross-origin',
// 在 origin-when-cross-origin 的基础上,当协议降级时,不携带 Referer
'strict-origin-when-cross-origin',
// 所有请求都携带上完整的 Referer
'unsafe-url',
// 无策略
''
])
function getHeaderValueFromOptions({ policy = ['no-referrer'] }) {
const tokens = typeof policy === 'string' ? [policy] : policy
tokensSeen.add(token)
})
// 多个协议可以通过逗号作兼容性降级(越靠后优先级越高)
return tokens.join(',')
}
function referrerPolicy(options = {}) {
const headerValue = getHeaderValueFromOptions(options)
return function referrerPolicyMiddleware(_req, res, next) {
res.setHeader('Referrer-Policy', headerValue)
next()
}
}
其实,在 HTML 中,无论是 Meta、Image、iFrame、Script 或是 Style 标签,都能设置 Referrer Policy
。Meta 标签设置的 Referrer Policy
对整个页面都有效果(但优先级最低)。
<meta name="referrer" content="origin" />
<a href="http://example.com" referrerpolicy="origin"></a>
<a href="http://example.com" rel="noreferrer"></a>
强制使用 HTTPS
通过设置请求头的 Strict-Transport-Security(STS)
,可以告诉浏览器,这个网站需要使用 HTTPS 而不是 HTTP 协议进行访问。浏览器每接收到这种请求后,会进行倒计时,在计时结束之前都不会将 HTTPS 降级回 HTTP8。
这个规范本身很好理解,通过 max-age
可以指定倒计时时间;includesSubDomains
指定子域的 HSTS
;preload
指定预加载内容的 HSTS
。
Strict-Transport-Security:
max-age=31536000;
includeSubDomains;
preload
const DEFAULT_MAX_AGE = String(180 * 24 * 60 * 60)
function getHeaderValueFromOptions(options) {
const directives = [`max-age=${options.maxAge || DEFAULT_MAX_AGE}`]
if (options.includeSubDomains === undefined || options.includeSubDomains) {
directives.push('includeSubDomains')
}
options.preload && directives.push('preload')
return directives.join('; ')
}
function strictTransportSecurity(options = {}) {
const headerValue = getHeaderValueFromOptions(options)
return function strictTransportSecurityMiddleware(_req, res, next) {
res.setHeader('Strict-Transport-Security', headerValue)
next()
}
}
可选用证书透明性策略
response.setHeader('Expect-CT' /* someValue */)
Expect-CT
响应头可以指定浏览器检测通讯时的证书是否存在于公共 CT 日志(也称作证书透明性策略9),避免中间人攻击10;也可以启动选择性报告,告知 CA 机构部分证书可能不合法,需要被回收。
Expect-CT:
// 指定浏览器上报证书失效的 URI
report-uri="<uri>";
// 指定浏览器应当拒绝于违反证书透明性策略的服务端建立连接
enforce;
// 在此期间,证书透明性策略相关信息可作缓存
max-age=<age>
自 2018 年 4 月,Chrome 强制要求所有 TLS 服务器证书都要符合 Chromium CT 政策。由于 Expect-CT 的 maxAge 最大可设置为 39 个月,39 个月之后,也就是 2021 年 6 月,Expect-CT
就作不上用咯... TODO
拒绝来自 PDF、Flash 的跨域请求
X-Permitted-Cross-Domain-Policies
这个非标准标头倒不是那么常见。这得说回 Flash、PDF 等文件中的请求。我们以 Adobe PDF Reader 官网的一张图为例11:
- 用户在 A 网站打开了某个 PDF;
- PDF 中包含了和 B 网站的通讯。这时,客户端判断这是跨域行为,于是请求 B 网站的
crossdomain.xml
文件(策略文件)作为跨域策略应对方案。
一个策略文件可能长这样:
<!-- https://www.taobao.com/crossdomain.xml -->
<cross-domain-policy>
<allow-access-from domain="*.taobao.com" />
<allow-access-from domain="*.taobao.net" />
<allow-access-from domain="*.taobaocdn.com" />
<allow-access-from domain="*.tbcdn.cn" />
<allow-access-from domain="*.alicdn.com" />
</cross-domain-policy>
其中,若请求响应头包含了 X-Permitted-Cross-Domain-Policies
,便可指定浏览器针对 crossdomain.xml
的应对行为。
const ALLOWED_PERMITTED_POLICIES = new Set([
// 只允许使用主策略(即只允许网站根目录的 crossdomain.xml)
'master-only',
// 仅当策略文件的响应头 Content-Type 为 text/x-cross-domain-policy 时,策略文件被允许使用
'by-content-type',
// 不使用任何位置的 crossdomain.xml
'none',
// 允许使用任何位置的 crossdomain.xml
'all'
])
function xPermittedCrossDomainPolicies(headerValue = 'none') {
return function xPermittedCrossDomainPoliciesMiddleware(_req, res, next) {
res.setHeader('X-Permitted-Cross-Domain-Policies', headerValue)
next()
}
}
可见 Helmet 的默认行为很简单,它使应用直接拒绝了来自 PDF、Flash 等非标准客户端的所有的跨域请求。
开启 CSP
还记得刚才提到 Chrome 关闭了 XSS Auditor
吗?既然不再使用 XSS Auditor
,那他就迫切需要一种更新的 XSS 防范手段,那就是我们提到的 CSP
标准12。CSP 提供了很多限制选项,涉及安全的各个方面,可以有效阻止一些基础的攻击手段。
CSP
既可以通过响应头指定,也可以通过 HTML 标签指定。
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';" />
一个典型的 CSP
头如下:
Content-Security-Policy: default-src 'none';
script-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==';
style-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==';
img-src 'self';
font-src 'nonce-XQY ZwBUm/WV9iQ3PwARLw==' fonts.gstatic.com;
object-src 'none';
block-all-mixed-content;
frame-ancestors 'none';
大致来说,CSP 相关以下几种安全行为:
- 指定内容有效域:如 script-src 限制了脚本的加载域,用以减少 XSS 攻击。
- 指定协议:使用 block-all-mixed-content 指定浏览器禁止加载 HTTP 内容;通过 upgrade-insecure-requests 指定浏览器将网站的 HTTP 协议升级为 HTTPS;
- 行为安全:通过 sandbox 指令指定浏览器禁止弹出窗口等行为。
- 启用违规报告:通过 report-uri 指定检测到违规行为时发送违规报告到指定地址。此外,如果仅报告而不指定拦截策略,可以仅使用
Content-Security-Policy-Report-Only
Header。
那么我们看看 Helmet 中默认配置的 CSP
策略:
const DEFAULT_DIRECTIVES = {
// 默认只允许使用本站资源(脚本、图片)
'default-src': ["'self'"],
// 限制 base 标签的 URI 为源地址
'base-uri': ["'self'"],
// 禁止通过 HTTP 协议加载内容
'block-all-mixed-content': [],
// 限制字体加载地址
'font-src': ["'self'", 'https:', 'data:'],
// 限制嵌入的外部资源
'frame-ancestors': ["'self'"],
// 限制图片加载地址
'img-src': ["'self'", 'data:'],
// 禁用 object 标签
'object-src': ["'none'"],
// 只从源地址加载脚本
'script-src': ["'self'"],
// 禁用内联脚本
// ?这里没能理解
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src-attr
// https://w3c.github.io/webappsec-csp/#directive-script-src-attr
'script-src-attr': ["'none'"],‘
// 限制样式表加载地址
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
// 不使用升级协议指令
// 据 MDN 介绍,该指令适用于需要重写大量不安全的旧版URL的网站,
// 所以默认不开启此指令
'upgrade-insecure-requests': []
}
最后,我们看兼容性... Caniuse 网站只提供了 CSP 1.0
的兼容性,我没有找到最新版本规范(CSP 3.0
)的兼容性数据 😝。
移除下载文件后的“打开”按钮
response.setHeader('X-Download-Options', 'noopen')
在下载文件时,X-Download-Options
Header 可以指定浏览器移除下载界面中的“打开”按钮。我猜测是因为通过这种方式的“打开”,某些浏览器会将页面上下文注入文件中,使页面容易收到攻击。有了解的朋友欢迎留言。
开启 DNS 预取
通过设置 X-DNS-Prefetch-Control
可以打开(或关闭)浏览器的 DNS 预请求功能。据 MDN 介绍,打开后,在图片数量较多的页面,能带来至少 5% 的加载速度提升。
至于 DNS Prefetch 具体是如何增强浏览器安全的... 我暂时没懂。我在 Issue 中问了 Helmet 的作者,他提供了关于 DNS Prefetch 安全性问题的额外的资料。请看这个 Issue。
response.setHeader('X-DNS-Prefetch-Control', 'on')
阅读更多
前端安全是一个非常宏大的话题。Hemlet 给你的 Express 应用带来的改进只是其中非常小的一部分内容。比如,它没有提及安全 Cookie 相关内容。
- 设置安全 Cookie:通过 Set-Cookie 可以指定浏览器设置安全 Cookie,该 Cookie 只能通过 HTTPS 发送至服务器;带 HttpOnly 的 Cookie 将不能被脚本访问;SameSite=Lax 则可以指定某 Cookie 不随跨域请求一起发送。13
如果你想了解更多关于 HTTP Secure Header 的详细内容,可以参考 OWASP Secure Headers Project。当然,MDN 上提供了关于前端安全问题更广泛的思路,有时间阔以深入学习一下:Web Security。以下是一些额外的可供探索的页面: