HTTP Header 有一大类叫 security header,即专门用来提高网站安全性的 header,都是 response header。你可以使用 mozilla observatory 或 security headers 检测自己网站的 header 有哪些需要改善的地方。
安全性 header 主要有以下几个,kejiweixun.com 已经设置了除 feature policy 之外的五个:
kejiweixun.com 是用 gatsby.js 生成的静态网站,用 nginx 作为 web server,使用腾讯云 CDN 提高网站访问速度,nginx 和 CDN 都可以对 response header 进行设置。
这篇文章主要讨论如何在 nginx 设置这些安全性 header,会顺带分享如何在 CDN 进行相对应的设置。
在 nginx 设置,其实就是修改 nginx 的配置文件,github 的 h5bp/server-configs-nginx 很有参考价值,根据这个 repo,我把我的 nginx.conf 配置文件修改为:
//nginx.conf文件
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65s;
gzip on;
gzip_comp_level 5;
gzip_types
application/javascript
application/x-javascript
application/json
application/xml
font/eot
font/otf
font/ttf
text/css
text/javascript
text/markdown
text/plain;
map $sent_http_content_type $x_frame_options {
~*text/html DENY;
}
map $sent_http_content_type $content_security_policy {
~*text/html "default-src 'self' *.kejiweixun.com; base-uri 'none'; form-action 'self' *.kejiweixun.com; frame-ancestors 'self' *.kejiweixun.com; script-src 'self' 'unsafe-inline' *.kejiweixun.com https://www.google-analytics.com; style-src 'self' *.kejiweixun.com 'unsafe-inline'; img-src 'self' *.kejiweixun.com data: https://www.google-analytics.com; object-src 'self'; connect-src 'self' *.kejiweixun.com https://www.google-analytics.com";
}
map $sent_http_content_type $referrer_policy {
~*text/html "same-origin";
}
include /etc/nginx/conf.d/*.conf;
}
nginx 的主配置文件叫 nginx.conf
,通常位于 /usr/local/nginx/conf
或 /etc/nginx
或 /usr/local/etc/nginx
。主配置文件中的一条条配置信息被称作 directive
,directive 有两种,分别是 simple directive 和 block directive。
simple directive 的格式是 directive 的名称 + 空格 + 参数 + 分号标点
,例如 include /etc/nginx/conf.d/*.conf;
就是一个 simple directive,意思是把 /etc/nginx/conf.d/*.conf
这个配置文件添加到 nginx.conf
中。
block directive 的格式也差不多,区别是分号标点 ;
换成了 {}
,{} 里可能有多个 simple directive。如果一个 block directive 包含有其他 directive,那这个 block directive 也被称作 context,例如 http {}
就是一个 context。
events 是一个和 http 同级的 directive,它们两个所在的 context 被称作 main context,http 这个 context 通常包含一个或多个叫 server 的 context,server 可能包含一个或多个叫 location 的 context,形成如下结构:
main
events
http
server
location
当然除此之外还有其他 context,例如 upstream 等。
在我的 nginx.conf 配置文件中,关于 header 的部分就是几个位于 http 里的 map directive,例如其中一个是:
map $sent_http_content_type $x_frame_options {
~*text/html DENY;
}
它的意思是,当 sent_http_content_type
这个变量的值是 ~*text/html
时,就创建一个叫 x_frame_options
的变量,并给这个变量赋值 DENY
,x_frame_options
这个变量接着将被 server context 引用。
前面我说过了,nginx.conf 可以通过 include 这个 directive 引入其他配置文件的配置信息,通常我们把这些配置文件放在与 nginx.conf 位于同一路径下的 conf.d 文件夹中,在这个例子中,这个其他配置文件就是 /conf.d/default.conf:
server {
listen [::]:80;
listen 80;
server_name www.kejiweixun.com;
return 301 $scheme://kejiweixun.com$request_uri;
}
server {
listen [::]:80;
listen 80;
server_name kejiweixun.com;
root /usr/share/nginx/html;
charset utf-8;
location / {
index index.html index.htm;
add_header cache-control "public,max-age=31536000,immutable" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options $x_frame_options always;
add_header Content-Security-Policy $content_security_policy always;
add_header Referrer-Policy $referrer_policy always;
add_header Access-Control-Allow-Origin "https://kejiweixun.com" always;
add_header Strict-Transport-Security "max-age=31536000" always;
}
location /sw.js {
add_header cache-control "public,max-age=0,must-revalidate" always;
add_header X-Content-Type-Options nosniff always;
add_header Access-Control-Allow-Origin "https://kejiweixun.com" always;
add_header Strict-Transport-Security "max-age=31536000" always;
}
location ~* \.(html|htm)$ {
add_header cache-control "public,max-age=0,must-revalidate" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options $x_frame_options always;
add_header Content-Security-Policy $content_security_policy always;
add_header Referrer-Policy $referrer_policy always;
add_header Access-Control-Allow-Origin "https://kejiweixun.com" always;
add_header Strict-Transport-Security "max-age=31536000" always;
}
location /page-data/ {
add_header cache-control "public,max-age=0,must-revalidate" always;
add_header X-Content-Type-Options nosniff always;
add_header Access-Control-Allow-Origin "https://kejiweixun.com" always;
add_header Strict-Transport-Security "max-age=31536000" always;
}
error_page 404 /404.html;
}
我没有在 nginx.conf 文件的 http context 中直接定义 server context,而是把它写在 /conf.d/default.conf 这个文件中,很多人都喜欢把 server context 从 http context 分离出来。
一个 http context 可以包含多个 server directive,我这里就包含了两个,不同 server 之间通过 listen 端口以及 server_name 的不同进行区分。我这里两个 server 都 listen 80 端口,但其中一个 server_name 是 www.kejiweixun.com,另一个是 kejiweixun.com。
第一个 server 的作用是把所有带 www 的链接 301 跳转到没有 www 的链接,所以第二个 server 才是最终起作用的 server。
第二个 server context 包含了 4 个 location context,我之所以写了 4 个 location,是因为我希望给这 4 个 location 分别单独设置 cache-control header。关于为什么要这样做,可以参考 gatsby.js 的官方文档。
cache-control header 不仅影响浏览器的缓存方式,也影响 CDN 的缓存方式。gatsby.js 部署到 CDN 时,可以让整个网站的文件都通过 CDN 提供,但这样做的话,以后每次对网站进行修改,例如添加一篇新的文章等等,都要手动进行缓存刷新,否则浏览器可能无法马上看到更新后的内容,因为如果不刷新,CDN 会继续向浏览器提供旧的缓存,所以 gatsby 建议我们把 html 文件、sw.js、所有位于 page-data 文件夹中的 json 文件,都设置为每次 request 均从源站获取。
max-age=0,must-revalidate
也可以写成 no-cache
,作用一样。no-cache 的作用并非“不要缓存”,恰恰相反,它要求浏览器或 CDN 缓存它收到的数据,不过在使用这些缓存之前,必须先验证缓存是否还有效,例如发送带有 If-Match
或 If-Modified-Since
header 的请求。
如果你希望浏览器不要缓存,应该用 no-store
,这样每一次请求,数据都直接来自服务器。no-store
并不能清空已经缓存下来的数据,可以加 max-age=0
。
如果你希望缓存设置只对浏览器有效,可以添加 private
属性,例如 Cache-Control: private,no-cache
,意思是内容可以缓存,但只能在浏览器缓存,不能在 CDN 等代理服务器缓存。
当然,其实我不需要在 nginx 设置这个 cache-control header,因为也可以在 CDN 设置,如下图所示:
但我希望对 header 拥有更充分的控制,而且我觉得源站才是真相的源头,所以自己设置了 cache-control header,于是有了四个 location context。
如果所有 add_header 都写在 location 之外且位于 server 之内,那这些 add_header 会自动应用于所有 location,但如果 location 内部同时也写了自己的 add_header,那 location 之外的 add_header 就会被这个 location 忽略,所以我需要在每个 location 之中分别写 add_header。
接下来分别说说以下 5 个安全性 header:
我把 X-Content-Type-Options 的值设置为 nosniff
,意思是 no sniff,作用是告诉浏览器不要进行 MIME sniffing (MIME 嗅探),即不要自作主张地更改 Content-Type 的值,而是完全按照开发者设定的 Content-Type 的值处理收到的文件。关于 MIME 嗅探是什么,以及它可能造成的安全问题,可以看看蔡同学的文章。
X-Frame-Options 的值可以是 sameorigin 和 DENY。sameorigin 的意思是,如果我在 kejiweixun.com 中的某个页面中,通过 <frame>
或 <iframe>
或 <embed>
或 <object>
插入 kejiweixun.com 的另一个页面,这个被插入的页面可以正常显示出来。
当值是 sameorigin 或 DENY 时,如果其他网站,比如 baidu.com 想通过 <frame>
或 <iframe>
或 <embed>
或 <object>
这些元素在它的页面中嵌入我的页面,那我的页面将无法正常显示,如下所示:
X-Frame-Options 可以用来防御 clickjacking,假设 kejiweixun.com 是一个银行网站,如果我没有设置 X-Frame-Options,那坏人就可以搭建一个领取免费奖品的网站,并用 <iframe>
等元素把银行网站的付款页面嵌入这个欺诈网站,但是这个付款页面用户是看不到的,这个 <iframe>
被领取奖品的页面遮挡在下面了,当用户点击他能看到的领取按钮时,实际可能是点了隐藏着的银行页面的转账按钮。
clickjacking 另一个用途是骗点赞,骗关注,例如把银行付款按钮换成微博点赞按钮等等。
在说 Referrer-Policy 这个 header 之前,需要先知道 Referer 这个 header,Referer 是在发生链接跳转时浏览器发送的 request header,用来告诉服务器我这个 request 来自哪里。
注意,Referer 是错误的英文单词,正确的写法应该是 Referrer,当人们发现这个单词写错的时候,Referer 已被广泛使用,来不及更正。Referrer-Policy 中的 Referrer 没有写错。
例如从 kejiweixun.com 首页点击链接跳转到 https://kejiweixun.com/about,那浏览器会发送一个带有 Referer header 的请求,这个 Referer 的值就是 kejiweixun.com,因为是从科技微讯的首页 kejiweixun.com 跳转过去的。
如果从 kejiweixun.com 跳转到 google.com,那 google.com 的服务器也可以获得 Referer: kejiweixun.com
这样的 header。
这个功能让 google analytic 能统计 A 网站有多少访客来自 B 网站,但因为 url 中可能包含一些敏感信息,不加思索地把 url 传给任何一个其他网站可能会泄漏用户的隐私信息。
Referer 是浏览器自动添加到 request 中的,但开发者可以通过 Referrer-Policy 这个 response header 要求浏览器不要乱来。 Referrer-Policy 有 8 种值,具体可以参考 MDN。
科技微讯的 url 其实没有什么敏感信息,所以我设置为 strict-origin-when-cross-origin
,表示从 kejiweixun.com 这个网站的任何页面跳转到 kejiweixun.com 的任何其他页面时,浏览器会发送一个带上 Referer header 的 request,其值是完整的 url,而跳转到其他通过 https 访问的域名时,也会带上 Referer header,但其值永远是 https://kejiweixun.com,不是完整的 url,另外,如果跳转的 url 是非 https,而是 http 域名,那不带上 Referer header。
简称 HSTS,用来告诉浏览器,喂,以后你访问我这个网站,麻烦你总是通过 https 访问,不要用 http。
所以如果用户输入的是 kejiweixun.com 或 http://kejiweixun.com 这样的网址,浏览器会自动转换成 https://kejiweixun.com,然后再向科技微讯的服务器发起请求。
所以如果你通过 kejiweixun.com 访问科技微讯,可能会看到 307 内部跳转:
腾讯云 CDN 有一个 "强制跳转 HTTPS" 的开关,其本质就是在 response header 中添加 Strict-Transport-Security 这个 header,这意味着在我的 nginx 服务器设置了这个 header,那腾讯云 CDN 即使没有启用这个功能,浏览器也会自动跳转 HTTPS。
HSTS 的值可能是:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
其中 max-age=31536000 告诉浏览器在接下来的 1 年时间内,都自动使用 https 访问我的网站,这是必需的值,当然时间你可以自定义。includeSubDomains 和 preload 是选填值,前者告诉浏览器 kejiweixun.com 及其所有子域名都启用 HSTS,后者告诉浏览器把我这个域名登记在一个目录中。这个目录就是所谓的 preload list,在这个 list 的域名都只能通过 https 访问。preload 要谨慎使用,因为上了 preload 名单再移除的话,可能要等很久。
这个 header 挺有趣,假如 kejiweixun.com 添加了 Feature-Policy: display-capture 'none'
这样的 header,那用户在手机上访问我这个网站时,就不能对我的内容截屏了。
Feature-Policy 顾名思义,就是设置这个网站可以使用浏览器的什么功能,不可以使用什么功能。现在浏览器功能非常丰富,可以调用相机,麦克风,支付,录屏,定位,震动等等,通过 Feature-Policy,我们可以告诉浏览器在访问我的这个网站时,禁用某些功能。
在这么多个安全性 header 中,CSP header 是最复杂的 header,它的值有无数种。
这是科技微讯的 CSP header:
content-security-policy: default-src 'self' https://*.kejiweixun.com; base-uri 'none'; form-action 'self' https://*.kejiweixun.com; frame-ancestors 'none'; script-src 'self' 'unsafe-inline' https://*.kejiweixun.com https://www.google-analytics.com; style-src 'self' https://*.kejiweixun.com 'unsafe-inline'; img-src 'self' https://*.kejiweixun.com data: https://www.google-analytics.com; object-src 'self'; connect-src 'self' https://*.kejiweixun.com https://www.google-analytics.com
为方便查看:
default-src 'self' https://*.kejiweixun.com;
base-uri 'none';
form-action 'self' https://*.kejiweixun.com;
frame-ancestors 'none';
style-src 'self' https://*.kejiweixun.com 'unsafe-inline';
script-src 'self' https://*.kejiweixun.com 'unsafe-inline' https://www.google-analytics.com;
img-src 'self' https://*.kejiweixun.com data: https://www.google-analytics.com;
connect-src 'self' https://*.kejiweixun.com https://www.google-analytics.com;
object-src 'self';
写法上注意以下几个特点:
directive 有很多个,其中比较值得关注的有:
default-src
作为 fetch directive 的 fallback,
即如果某些 fetch directive 没有声明,
就把这些 direcitve 的值视为等于 default-src 的值.
script-src
告诉浏览器可以执行来自哪些地方的 script
style-src
告诉浏览器可以处理来自哪些地方的 style
img-src
告诉浏览器可以显示来自哪些地方的图片
object-src
告诉浏览器可以显示来自哪些 url 的 <object>
或 <embed> 或 <applet> 等元素的内容
connect-src
告诉浏览器可以 fetch 或 XMLHttpRequest 哪些 url,等;
base-uri
限制 <base> 元素的 href 属性的值;
form-action
限制 <form> 元素的 action 属性的值;
frame-src
告诉浏览器可以显示来自哪些地方的 <frame>,<iframe> 等元素的内容
frame-ancestors
限制 <frame> 或 <iframe> 或 <object> 或 <embed> 或 <applet>
这些元素的父元素可以是什么,
作用涵盖了 x-frame-options,
但旧浏览器可能不支持 CSP,
所以建议同时设置 x-frame-options.
由于 fetch directive 有 default-src 作为 fallback,所以不需要定义每一个 fetch directive 的值,但如果某个 fetch directive 的值不能等于 default-src 的值,那要单独声明,当某个 fetch directive 单独声明之后,它会忽略 default-src 的值。
对于其他 directive,例如 form-action 和 frame-ancestors,不声明就代表允许所有 url。
需要注意 frame-ancestors 和 x-frame-options 的区别。科技微讯同时设置了这两个 header,当某个第三方域名通过 <iframe>
等元素嵌套科技微讯的某个网页时,chrome 会显示:
而 directive 的值就更多了,有无数种可能,主要分为以下几类:
https://*.kejiweixun.com
科技微讯的所有子域名,但不包括 https://kejiweixun.com
https://kejiweixun.com
精确匹配,只匹配这一个域名的 url
kejiweixun.com:443
可以规定端口
https: 或 http: 或 data: 或 blob: 等等
可以只设定协议,匹配所有这种协议的 url
*
匹配所有 url
'none'
屏蔽所有 url
'self'
匹配当前页面的域名元素,会同时检测域名,端口,协议是否匹配,
都匹配才是匹配
'unsafe-inline'
详见下文
'unsafe-eval'
匹配 eval(),也是不安全
'strict-dynamic'
要撘配 hash 或 nonce 使用,具体看 mdn.
'<hash-algorithm>-<base64-value>'
详见下文
'nonce-<base64-value>'
详见下文
匹配 inline 的 script 或 style,因为 inline 的 stript 或 stype 不安全,所以在前面加了 unsafe,提醒开发者如果你这样设置,不够安全。
注意这里的 inline style 不仅仅指 html 元素的 style 属性:
<h1 style="color: red">科技微讯</h1>
还包括 <style>
元素等:
<style>
{
background: red;
}
</style>
这里的 inline script 包括 <script>
元素内部的 script:
<script>
var inline = 1;
</script>
也包括 inline event handler 等:
<button onclick="alert('科技微讯')">点我!</button>
\<hash-algorithm>-<base64-value\>
这是一个 hash source 的例子:
'sha256-U53QO64URPPK0Fh7tsq0QACAno68LrPc5G6Avyy07xs='
这个 hash source 表示 alert('Hello world!');
。对于 inline 的 style 或 script,可以把这些 style 或 script 用 hash 算法加密,然后添加到 directive 的值中,一个 hash source 匹配一个 inline script 或 style,如果你有无数个 inline script 或 style 恐怕不可行。
网上有 CSP hash source 生成器,把你的 inline style 或 inline script 复制过去,会自动生成一个 base64-encoded 的 hash 值。
如果你想手动生成,可以用这个 node 程序:
const crypto = require("crypto");
const result = crypto
.createHash("sha256")
.update("alert('Hello world!');","utf-8")
.digest("base64");
console.log(result);
这是一个 nonce source 的例子:
<script nonce="这里替换成一个随机数">
alert("Hi!");
</script>
nonce 在密码学语境下指的是一个只使用一次的数字。nonce source 类似 hash source,区别是 nonce source 不是对 inline style 或 script 进行加密,而是给这些包含 inline style 或 script 的 html 元素添加一个 nonce 属性,然后把这个属性添加到 directive 的值中。随机数只能用一次,响应浏览器的每次请求都要使用一个新的随机数。
unsafe-inline
'unsafe-inline' 是一个不安全的 source,但科技微讯网站使用了很多 inline 的 style 或 script,所以我还是选择用 unsafe-inline 了。
不过正如前面所说过的,inline 的 style 或 script 可以用 hash source 或 nonce source 代替。有人给 gatsby 开发了一款名为 gatsby-plugin-csp 的插件,可以自动为这些 inline style 或 script 生成 hash,并添加到 <meta>
元素中,例如:
<meta
http-equiv="Content-Security-Policy"
content="base-uri 'self'; default-src 'self'; script-src 'self' 'sha256-ctSi8cL8mD4Z+B6mC6NGy8pBg8EubnSdgcor00NYCtU=';style-src 'self' 'sha256-MtxTLcyxVEJFNLEIqbVTaqR4WWr0+lYSZ78AzGmNsuA='; object-src 'none'; form-action 'self'; font-src 'self' data:; connect-src 'self'; img-src 'self' data:;"
/>
把 CSP 作为属性添加到 <meta>
元素是实现 CSP 的另一种方法,这样就不用在 header 定义了,但用 header 定义通常是更好的方法。
需要注意的是,hash source 和 nonce source 都只适用于 <style></style>
<script></script>
这种 inline style 或 script,不适用于 style 属性和 inline event handler。而我猜几乎每位 gatsby 用户都会使用的 gatsby-image 恰恰使用了 style 属性处理图片,所以 gatsby-plugin-csp 这个插件并不能应对所有情况,还是要用 'unsafe-inline'。
如果你希望严格遵守 CSP,在写代码的时候就不要用 style 属性,也不要写 inline event handler,例如 event handler 本来是这样写的:
<button onclick="myFunction()">点我</button>
要改成这样:
<!--html-->
<button id="myButton">点我</button>
<script src="script.js"></script>
和:
//script.js
document.getElementById("myButton").addEventListener("click",myFunction);
function myFunction() {
console.log("asd");
}
这款插件并不适合我,所以我选择使用 unsafe-inline
。gatsby.js 用户已经在讨论怎么让 gatsby 更好地实现 CSP,但至少到目前为止还没有很好的方法。
对于 web 开发而言,HTTP header 是非常重要的知识点,一个 header 看起来很简单,却可能对网站产生重要影响。我很早就开始使用腾讯云 CDN,但直到现在才大体明白原来 CDN 里的各项设置其实是通过 header 影响网站的行为,终于明白这些设置选项都是什么意思,有什么用。
最后关于 HTTP header,可以再看看 Scott Helme 和 Andrew Betts 写的文章。