强化网站安全 - 第一篇:安全相关的Headers (Hardening Website Security – Part 1: HTTP Security Headers)

原文:Hardening Website Security – Part 1: HTTP Security Headers

简介

感觉每周都会出现新的个人信息被盗事件,原因是某一家公司网站被了。

大部分攻击都是通过社会工程学诱骗某些用户主动提交一些信息,黑客借助这些信息获取更高权限并最终获取个人资料。

如果你想要在21世纪运营一个安全的网站,你需要遵循一些安全准则,我会写一个系列专栏说明这些安全准则,这是第一篇。

本文不会介绍以下内容

因为网络基础架构的安全配置内容太多(自托管或者VPS,OS,管理中台,防火墙等)而且细微差异众多,我不会在这个系列中介绍。我可能后面会专门写相关文章介绍那些内容。

免责声明

文章内容来自于我多年的工作经验、尝试,以及(频繁)犯错的总结。在正常经营的网站忽略HTTP headers 设置(极有)可能导致一些意外的甚至是灾难性的后果。本文所写内容都是个人依据自身知识水平总结的最佳实践,但是涉及到安全问题,我强烈建议读者在对你自己的系统或网站做任何改动之前确保你知道自己在干什么以及这么干的后果。本文所有的代码仅作示例展示,可能并不完善甚至有一些错误。读者从网页上拷贝粘贴使用代码时需要格外小心,这个网站也包含在内。Int64 Software Ltd公司所有员工与企业法人代表不对因故意或无意误用其帖子和文章中所提供信息而造成的损害承担任何责任。

概述

当Web服务器接收一个标准的 HTTP Get 请求之后,它会把请求的资源和一些Header信息一起返回。这对于浏览器如何处理响应数据来说是必要的,比如资源的编码,内容长度等。

第一篇我们先来看一下安全相关的 Header有哪些,他们的作用以及如何设置他们。

配置服务器

网站托管环境不同,设置Header的方法不同。下面是一些常用的比较流行的服务器软件/编程语言的Header配置方法。

Apache Web Server

Apache的虚拟主机配置文件位于.htaccess.httpd.conf,使用以下指令设置:

1
Header set [HeaderName] [HeaderValue]

Nginx

在Nginx 的config 目录下,打开nginx.conf,添加以下内容:

1
add_header [HeaderName] "[HeaderValue]";

IIS

(译者加: 在每个虚拟主机目录下)打开Web.Config文件,在<system.webServer>节点下添加<customHeaders>节点:

1
2
3
4
5
<httpProtocol>
<customHeaders>
<add name="[HeaderName]" value="[HeaderValue]" />
</customHeaders>
</httpProtocol>

PHP

PHP 可以在文件顶部(在任意输出内容设置之前,包括空白)使用header 方法设置:

1
header('[HeaderName]: [HeaderValue]', true);

备注:一个请求只需要设置一次Header,不需要在每个包含的PHP文件里面配置。true 表示会覆盖已存在的同名 Header 。

Asp.Net

global.asax 文件增加以下代码:

1
2
3
4
protected void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext.Current.Response.AddHeader("[HeaderName]", "[HeaderValue]");
}

Headers

X-Frame-Options

X-Frame-Options用来防止 点击劫持,点击劫持指的是攻击者试图在用户浏览/使用网页过程中在诸如点击按钮等事件中执行一些非预期的操作。其中常见的一种攻击方式是 攻击者在他们自己的钓鱼网站上通过iframe 嵌入你的网页,然后在界面上覆盖一个透明的区域,用户以为在浏览你的网页,其实用户提交的信息已经被攻击者盗取。
通过设置X-Frame-Options 你可以告诉浏览器当前网页不允许通过iframe 加载。
该选项有三个可选值:

DENY

设置为DENY可以阻止任何其他网页通过iframe嵌入你的网页。除非你的网页明确需要被嵌入,否则请设置为这个值。

SAMEORIGIN

设置为SAMEORIGIN会允许你自己的网站(同一域名),但是会阻止其他域(包括其他子域)嵌入你的网页。

ALLOW-FROM <uri&gt;

这种方式将阻止除指定URI以外的其他所有网页使用iframe加载当前网页(比如ALLOW-FROM https://myothersite.com)。

注意:该选项的浏览器支持性有限

推荐配置

X-Frame-Options 应当总是设置为DENY,除非有明确的需要配置为其他值

其他说明

X-Frame-Options 选项目前仍然被广泛支持,但是稍后即将讨论的CSP的frame-src选项可以替代这个功能。但是IE对CSP的支持从IE11 开始,而IE8以上已经支持X-Frame-Options,所以它仍然需要配置。

X-XSS-Protection

X-XSS-Protection 被设计用于阻止跨站简本攻击(XSS),X-XSS-Protection选项将开启浏览器的XSS过滤功能。XSS的攻击者通过注入恶意脚本(例如通过未经安全过滤的用户输入框)到网页,在用户浏览时影响网页正常展示或者偷取浏览者个人数据。
最近有一起XSS攻击事件,攻击者通过脆弱的广告网络注入恶意脚本。攻击者有可能会把用户的付款信息转向他自己而不是用户所使用网页的提供者。
X-XSS-Protection可以通知浏览器过滤并阻止XSS攻击进而降低安全风险,它有四种取值:

0.

不启用(不推荐)

1.

启用,如果检测到攻击代码,将会删除恶意代码并继续展示网页。通常是默认值。

1; mode=block

启用,如果检测到攻击代码,将会阻止网页继续展示。

1; report=<reporting-uri>

启用,如果检测到攻击代码,将会阻止并清理网页,然后向指定的uri报告恶意攻击事件。

推荐配置

X-XSS-Protection应该设置为1,然后看情况开启block或report选项

其他说明

X-Frame-Options类似,CSP可以设置更强大和安全的策略,可以彻底禁止页面内联脚本的执行。稍后讨论。

X-Content-Type-Options

X-Content-Type-Options只有一个取值nosniff,也只有一个目的:阻止浏览器使用Content-Type以外的类型处理当前内容。
当浏览器收到一个文件之后,Content-Type会告诉浏览器这个文件的类型,浏览器自己也会使用多种技术做内容嗅探(或者媒体类型嗅探或MIME嗅探),搞清楚文件的具体类型然后相应处理。
然后,在此过程中可能出现用户期望的文件类型与事实不符的情况,这有可能导致跨站脚本攻击或恶意软件分发。

推荐配置

上面已经说了,应当总是设置X-Content-Type-Options为nosniff,阻止浏览器默认行为。

Strict-Transport-Security

HTTP Strict Transport Security,通常简称为HSTS,用来通知浏览器使用安全的HTTPS连接而不是未加密的 HTTP 协议。
有两种取值:

max-age=<MaxAgeInSeconds>

表示在未来的一段时间(单位为秒)内浏览器需要记住当前站点只能通过 HTTPS 访问。

includeSubDomains

可选值,如果指定,表示当前站点的所有子域名也需要使用 HTTPS 连接访问

preload(译者加)

可选值。
谷歌维护着一个 HSTS 预加载服务。按照如下指示成功提交你的域名后,浏览器将会永不使用非安全的方式连接到你的域名。虽然该服务是由谷歌提供的,但所有浏览器都有使用这份列表的意向(或者已经在用了)。但是,这不是 HSTS 标准的一部分,也不该被当作正式的内容。

推荐配置

Strict-Transport-Security 应当设置为稍微大一些的值例如1年(31536000 秒)。includeSubDomains 选项可以看情况设置。

1
Strict-Transport-Security: max-age=31536000; includeSubDomains

Content-Security-Policy

最重要也最复杂的Header可能就是CSP,很多浏览器已经支持。如果配置得当,单一的CSP就可以为你的网站保驾护航。但是如果配置错误,也可能影响巨大。
CSP的功能主要就是指示浏览器可以从哪里下载脚背以及服务器资源。
一个CSP Header包含两部分:一个指令以及一个资源列表。指令指示当前设置的资源类型,资源列表限制当前指令可以下载的源。
注意,CSP可以包含多个策略,你可以在一个Header里面通过分号(;)分隔多条指令,也可以设置多个CSP Header。
比如,我想要配置JS文件从我们自己的服务器和一个JS 存储仓库(js.example.com)下载,样好似文件仅从我们自己的服务器下载,我可以通过单一Header 设置:

1
header("Content-Security-Policy: script-src 'self' js.example.com; style-src 'self';", true);

或者通过多个Header 设置:

1
2
header("Content-Security-Policy: script-src 'self' js.example.com;", false);
header("Content-Security-Policy: style-src 'self';", false);

注意最后一个参数表示替换已存在同名Header需要改为false,用来同时设置多个同名Header。

指令类型

default-src

default-src 代表默认指令,如果正在请求的资源类型未明确定义,将会使用 default-src 策略。如果网页有图片请求,但是img-src没有配置,那么就会使用default-src的配置。

child-src(已弃用)

之前用来配置 web workers 以及嵌套浏览器执行环境比如 iframe。然而,frames 和 workers 可能需要单独配置不同策略,这个选项已经被frame-srcworker-src取代。

connect-src

限制脚本可以请求的源(a标签,pings,fetch,XMLHttpRequest,WebSocket,EventSource)

font-src

限制字体可以请求的源(Google Fonts等)

frame-src

限制<frame><iframe>可以加载的源

frame-ancestors

限制当前页面可以被<frame><iframe>加载的源,设置为'none'效果与 X-Frame-Options: DENY 相同

img-src

限制图片可以加载的源

manifest-src

限制 Application manifest 文件可以加载的源

media-src

限制媒体(<audio> , <video>, <track>)可以加载的源

object-src

限制 <object>, <embed>, and <applet> 可以加载的源

prefech-src

限制预请求或预渲染的源

script-src

限制JS脚本的源。应当按需求配置为'self'以及可信的第三方CDN源。永远不要允许'unsafe-inline',否则内联脚本的执行可能会导致XSS攻击,从而破坏整个CSP配置的功能。

style-src

限制CSS样式的源

webrtc-src

限制WebRTC(Web Real-Time Communications)可以连接的源

worker-src

限制Worker/SharedWorker/ServiceWorker脚本的源

form-action

限制表单提交的源

report-uri

指定破坏CSP规则上报的uri

block-all-mixed-content

如果页面是通过HTTPS加载的,跳过所有HTTP资源请求

*

允许所有data stream(blob:, data:, filesystem:)以外的源

‘none’

禁止当前资源类型从任意源加载

‘self’

当前页面所在源(同协议,同主机名,同端口)

Data:

数据流(例如base64 编码的图片地址)

domain.example.com

某一个指定域名

*.example.com

某一个指定域名以及所有子域名

https://domain.example.com

某一个指定域名并且只能通过HTTPS 连接

https:

任意通过HTTPS连接的域名

‘unsafe-inline’

允许内联代码,包括内联样式或脚本块(请尽量避免使用)

‘unsafe-eval’

允许javascripteval()方法

‘nonce-<noncevalue>’

可以在script/style标签上增加一个随机数,只有随机数与noncevalue匹配时,相应标签代码才可以执行。注意这个策略已经破解,可能不安全。

‘sha256-<hashvalue>’

script/style的SHA256哈希值与hashvalue匹配,相应代码才可以执行

推荐配置

这一项很难有一个通用的确切值,因为需要根据每个网站具体配置。我的做法(在任何安全相关的问题)是尽可能把规则配的具体,然后找到冲突的点,解决冲突或者放松规则,直到所有的配置都正常工作。这里也可以这么做,但是请考虑以下情况:
比如style-src,我们很容易就会配置为'self'策略,表示只能从本域加载 (.css) 文件。但你的代码里面可能会有少量内联样式,这时候怎么处理呢?最好是把这些内联样式都移到某一个文件里面,这并不总是可以很简单的完成,你可能会需要允许'unsafe-inline'
我觉得一个比较好的方式是通过一项项查看我的某个项目的CSP配置,说明一下我为什么这么配置,以及这么配置的负面影响。

1
default-src 'none'; 

默认配置为拒绝所有未指定资源类型的任意加载源。考虑到我已经仔细检查了其他所有资源类型的加载策略,不应该有资源会回退到这个配置,如果有,那么这个资源一定不是由我添加的。注意:其他人在新增资源但是无法加载时可能来找你麻烦,因此很多人会设置为'self'

1
script-src 'self' https://js.stripe.com;

所有的脚本都已经打包、压缩,因此不应该出现内联脚本,应当配置为'self'。另外,我使用 Stripe 作为支付网关,所以要允许 Stripe 的脚本库。

1
connect-src 'self' https://api.stripe.com; 

我的应用大量使用Ajax请求,所以需要添加'self'。Stripe 也需要使用脚本请求接口,所以也添加了 Stripe 的服务域名。

1
img-src 'self' data:; 

80年代的网页有一个明显的问题就是所有的图片都是由自己的服务器托管,所以需要'self'。当然,网页里面也会用到内联SVG图像,所以包含了'data:'

1
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; 

样式主要都是自己服务器托管,少量使用 Google Fonts,所以添加了'self'和Google Fonts。很惭愧我(虽然很少但是偶尔会)也使用了页面内联样式,所以我添加了'unsafe-inline'。注意:已经有证据证明可以通过CSS注入读取表单信息,所以你需要额外注意使用这个策略可能导致的应用程序脆弱性。

1
font-src 'self' https://fonts.gstatic.com;

这个也相当简单,我要用到我们自己的和 Google 提供的字体,所以添加了'self'和Google的字体库。

1
frame-src 'self' https://*.stripe.com https://stripe.com; 

这个略微有些复杂,Stripe 允许使用 3D Secure 保护卡的授权登录。不幸的是,对方推荐的实现方式是使用 Iframe(我也讨厌)。所以我这里添加 Stripe 作为支付网关,'self'做为授权完成之后 Iframe 的回跳地址。
如果你也使用Stripe,你可能知道他们的文档说明只需要添加 https://js.stripe.com 作为 frame source,但是我操作的过程中发现一些问题,我正在与他们的技术支持团队合作把这个策略尽量收紧。

1
frame-ancestors 'none';

之前已经提到,这项配置可以禁止当前网页被任何网站通过 Iframe 嵌入加载。注意:我在 3D Secure 的授权回跳页面上面把这项配置放送到'self',这样我自己的网站才可以通过Iframe加载回跳页面。

1
report-uri /csp-report.php;

所有破坏CSP规则的异常都会被上报(由客户端浏览器)到这个地址,记录下来以后分析使用。

1
block-all-mixed-content;

最后,尽管我没有任何混合内容(译者:与页面协议不一致的加载方式,例如页面使用HTTPS,而某一图片使用HTTP),防止意外我还是添加了这条禁止策略。

其他指令的注意点:   
绝大部分其他指令策略都会回退到 default-src 然后被禁止。有一个例外:worker-src 在很多浏览器(Chrome 59以上会忽略child-src直接回退到default-src, Edge 17 会忽略 script-src)中会回退到 child-src -> script-src 。
不设置 plugin-types 指令,在技术上会默认允许所有的扩展类型 (<embed>, <object>, <applet>),但同时我们没有配置 object-src,这项策略会降级到 default-src 从而在加载层面禁止所有的扩展。

代码示例

Apache Web Server

1
2
3
4
5
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
Header set X-Content-Type-Options "nosniff"
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header set Content-Security-Policy "default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;"

Nginx

1
2
3
4
5
add_header X-Frame-Options "SAMEORIGIN"
add_header X-XSS-Protection "1; mode=block"
add_header X-Content-Type-Options "nosniff"
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"
add_header Content-Security-Policy "default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;"

IIS

1
2
3
4
5
6
7
8
9
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="X-XSS-Protection" value="1; mode=block" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains"/>
<add name="Content-Security-Policy" value="default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;" />
</customHeaders>
</httpProtocol>

PHP

1
2
3
4
5
6
7
header('[HeaderName]: [HeaderValue]', true);

header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
header('X-Content-Type-Options: nosniff');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('Content-Security-Policy: default-src \'none\'; script-src \'self\' https://js.stripe.com; connect-src \'self\' https://api.stripe.com; img-src \'self\' data:; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; font-src \'self\' https://fonts.gstatic.com; frame-src \'self\' https://*.stripe.com https://stripe.com; frame-ancestors \'none\'; report-uri /csp_report.php; block-all-mixed-content;');

ASP.NET

1
2
3
4
5
6
7
8
9
protected void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext.Current.Response.AddHeader("X-Frame-Options", "SAMEORIGIN");
HttpContext.Current.Response.AddHeader("X-XSS-Protection", "1; mode=block");
HttpContext.Current.Response.AddHeader("X-Content-Type-Options", "nosniff");
HttpContext.Current.Response.AddHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
HttpContext.Current.Response.AddHeader("Content-Security-Policy",
"default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;");
}

下集预告

请先阅读有关使用 HSTS Preloading 的附录,然后继续阅读第二篇:Cookie攻防实战
关注作者:Twitter
译者微薄:新浪微博

刊误

如果你有任何意见或建议,请联系:support@int64software.com
译者邮箱:mrzzcn@gmail.com