為你的網站帶上帽子 — 使用 helmet 保護 Express 應用

lsvih發表於2017-12-04

為你的網站帶上帽子 — 使用 helmet 保護 Express 應用

Express 基於 Node.js,是一款用於構建 Web 服務的優秀框架。它很容易上手,且得益於其中介軟體的概念,可以很方便地進行配置與擴充。儘管現在有各種各樣的用於建立 Web 應用的框架,但我的第一選擇始終是 Express。然而,直接使用 Express 不能完全遵循安全性的最佳實踐。因此我們需要使用類似 helmet 的模組來改善應用的安全性。

部署

在開始之前,請確認你已經安裝好了 Node.js 以及 npm(或 yarn)。你可以在 Node.js 官網下載以及檢視安裝指南

我們將以一個新的工程為例,不過你也可以將這些功能應用於現有的工程中。

在命令列中執行以下命令建立一個新的工程:

mkdir secure-express-demo
cd secure-express-demo
npm init -y
複製程式碼

執行以下命令安裝 Express 模組:

npm install express --save
複製程式碼

secure-express-demo 目錄下建立一個名為 index.js 的檔案,加入以下程式碼:

const express = require('express');
const PORT = process.env.PORT || 3000;
const app = express();

app.get('/', (req, res) => {
  res.send(`<h1>Hello World</h1>`);
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
複製程式碼

儲存檔案,試執行看看它是否能正常工作。執行以下命令啟動服務:

node index.js
複製程式碼

訪問 http://localhost:3000,你應該可以看到 Hello World

hello-world.png

檢查 Headers

giphy.gif

現在讓我們通過增加與刪除一些 HTTP headers 來改善應用安全性。你可以用一些工具來檢查它的 headers,例如使用 curl 執行以下命令:

curl http://localhost:3000 --include
複製程式碼

--include 標誌可以讓其輸出 response 的 HTTP headers。如果你沒有安裝 curl,也可以用你最常用瀏覽器開發者工具的 network 皮膚代替。

你可以看到在收到的 response 中包含的以下 HTTP headers:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 20
ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU"
Date: Wed, 01 Nov 2017 13:36:10 GMT
Connection: keep-alive
複製程式碼

一般來說,由 X- 開頭的 header 是非標準頭部。請注意那個 X-Powered-By 的 header,它會暴露你使用的框架。對於攻擊者來說,這可以降低攻擊成本,因為他們只專注攻擊此框架的已知漏洞即可。

戴上頭盔(helmet)

giphy.gif

來看看如果我們使用 helmet 會發生什麼。執行以下命令安裝 helmet

npm install helmet --save
複製程式碼

helmet 中介軟體加入你的應用中。對 index.js 進行如下修改:

const express = require('express');
const helmet = require('helmet');
const PORT = process.env.PORT || 3000;
const app = express();

app.use(helmet());

app.get('/', (req, res) => {
  res.send(`<h1>Hello World</h1>`);
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
複製程式碼

這樣就使用了 helmet 的預設配置。接下來看看它做了什麼事情。重啟服務,再次通過以下命令檢查 HTTP headers:

curl http://localhost:3000 --inspect
複製程式碼

新的 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: 1; mode=block
Content-Type: text/html; charset=utf-8
Content-Length: 20
ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU"
Date: Wed, 01 Nov 2017 13:50:42 GMT
Connection: keep-alive
複製程式碼

首先值得慶祝的是 X-Powered-By header 不見了。但現在又多了好些新的 header,它們是做什麼的呢?

X-DNS-Prefetch-Control

這個 header 對增加安全性並沒有太大作用。它的值為 off 時,將關閉瀏覽器對頁面中 URL 的 DNS 預讀取。DNS 預讀取可以提高你的網站的效能,根據 MDN 描述,它可以增加 5% 或更高的圖片載入速度。不過開啟這項功能也可能會使使用者在多次訪問同一個網頁時快取出現問題。

譯註:快取問題未查到資料,如果您瞭解這塊請留言

它的預設值是 off,如果你希望通過它提升效能,可以在呼叫 helmet() 時傳入 { dnsPrefetchControl: { allow: true }} 開啟 DNS 預讀取。

X-Frame-Options

X-Frame-Options 可以讓你控制頁面是否能在 <frame/><iframe/> 或者 <object/> 之類的頁框內載入。除非你的確需要通過這些方式來開啟頁面,否則請通過下面的配置完全禁用它:

app.use(helmet({
  frameguard: {
    action: 'deny'
  }
}));
複製程式碼

所有的現代瀏覽器都支援 X-Frame-Options。你也可以通過稍後將介紹的內容安全策略來控制它。

Strict-Transport-Security

它也被稱為 HSTS(嚴格安全 HTTP 傳輸),用於確保在訪問 HTTPS 網站時不出現協議降級(回到 HTTP)的情況。如果使用者一旦訪問了帶有此 header 的 HTTPS 網站,瀏覽器就會確保將來再次訪問次網站時不允許使用 HTTP 進行通訊。此功能有助於防範中間人攻擊。

有時,當你使用公共 WiFi 時嘗試訪問 https://google.com 之類的門戶網頁時就能看到此功能運作。WiFi 嘗試將你重定向到他們的入口網站去,但你曾經通過 HTTPS 訪問過 google.com,且它帶有 Strict-Transport-Security 的 header,因此瀏覽器將阻止重定向。

你可以訪問 MDN 或者 OWASP wiki 檢視更多相關資訊。

X-Download-Options

這個 header 僅用於保護你的應用免受老版 IE 漏洞的困擾。一般來說,如果你部署了不能被信任的 HTTP 檔案用於下載,使用者可以直接開啟這些檔案(而不需要先儲存到硬碟去)並且可以直接在你 app 的上下文中執行。這個 header 可以確保使用者在訪問這種檔案前必須將其下載到本地,這樣就能防止這些檔案在你 app 的上下文中執行了。

你可以訪問 helmet 文件MSDN 博文檢視更多相關資訊。

X-Content-Type-Options

一些瀏覽器不使用伺服器傳送的 Content-Type 來判斷檔案型別,而使用“MIME 嗅探”,根據檔案內容來判斷內容型別並基於此執行檔案。

假設你在網頁中提供了一個上傳圖片的途徑,但攻擊者上傳了一些內容為 HTML 程式碼的圖片檔案,如果瀏覽器使用 MIME 嗅探則會將其作為 HTML 程式碼執行,攻擊者就能執行成功的 XSS 攻擊了。

通過設定 header 為 nosniff 可以禁用這種 MIME 嗅探。

X-XSS-Protection

此 header 能在使用者瀏覽器中開啟基本的 XSS 防禦。它不能避免一切 XSS 攻擊,但它可以防範基本的 XSS。例如,如果瀏覽器檢測到查詢字串中包含類似 <script> 標籤之類的內容,則會阻止這種疑似 XSS 攻擊程式碼的執行。這個 header 可以設定三種不同的值:011; mode=block。如果你想了解更多關於如何選擇模式的知識,請檢視 X-XSS-Protection 及其潛在危害 一文。

升級你的 helmet

以上只是 helmet 提供的預設設定。除此之外,它還可以讓你設定 Expect-CTPublic-Key-PinsCache-ControlReferrer-Policy 之類的 header。你可以在 helmet 文件 中查詢更多相關配置。

保護你的網頁免受非預期內容的侵害

giphy.gif

跨站指令碼執行對於 web 應用來說是無法根絕的威脅。如果攻擊者可以在你的應用中注入並執行程式碼,其後果對於你和你的使用者來說可能是一場噩夢。有一種能試圖阻止在你網頁中執行非預期程式碼的方案:CSP(內容安全策略)。

CSP 允許你設定一組規則,以定義你的頁面能夠載入資源的來源。任何違反規則的資源都會被瀏覽器自動阻止。

你可以通過修改 Content-Security-Policy HTTP header 來指定規則,或者你不能改 header 時也可以使用 meta 標籤來設定。

這個 header 類似於這樣:

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';
複製程式碼

在這個例子中,你可以看到我們只允許從自己的域名或者 Google Fonts 的 fonts.gstatic.com 來獲取字型;只允許載入本域名下的圖片;只允許載入不指定來源,但必須包含指定 nonce 值的指令碼及樣式檔案。這個 nonce 值需要用下面這樣的方式指定:

<script src="myscript.js" nonce="XQY ZwBUm/WV9iQ3PwARLw=="></script>
<link rel="stylesheet" href="mystyles.css" nonce="XQY ZwBUm/WV9iQ3PwARLw==" />
複製程式碼

當瀏覽器收到 HTML 時,為了安全起見它會清除所有的 nonce 值,其它的指令碼無法得到這個值,也就無法新增進網頁中了。

你還可以禁止所有在 HTTPS 頁面中包含的 HTTP 混合內容和所有 <object /> 元素,以及通過設定 default-srcnone 來禁用一切不為圖片、樣式表以及指令碼的內容。此外,你還可以通過 frame-ancestors 來禁用 iframe。

你可以自己手動去編寫這些 header,不過走運的是 Express 中已經有了許多現成的 CSP 解決方案。helmet 支援 CSP,但 nonce 需要你自己去生成。我個人為此使用了一個名為 express-csp-header 的模組。

安裝及執行 express-csp-header

npm install express-csp-header --save
複製程式碼

為你的 index.js 新增並修改以下內容,啟用 CSP:

const express = require('express');
const helmet = require('helmet');
const csp = require('express-csp-header');

const PORT = process.env.PORT || 3000;
const app = express();

const cspMiddleware = csp({
  policies: {
    'default-src': [csp.NONE],
    'script-src': [csp.NONCE],
    'style-src': [csp.NONCE],
    'img-src': [csp.SELF],
    'font-src': [csp.NONCE, 'fonts.gstatic.com'],
    'object-src': [csp.NONE],
    'block-all-mixed-content': true,
    'frame-ancestors': [csp.NONE]
  }
});

app.use(helmet());
app.use(cspMiddleware);

app.get('/', (req, res) => {
  res.send(`
    <h1>Hello World</h1>
    <style nonce=${req.nonce}>
      .blue { background: cornflowerblue; color: white; }
    </style>
    <p class="blue">This should have a blue background because of the loaded styles</p>
    <style>
      .red { background: maroon; color: white; }
    </style>
    <p class="red">This should not have a red background, the styles are not loaded because of the missing nonce.</p>
  `);
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
複製程式碼

重啟服務,訪問 http://localhost:3000,可以看到一個帶有藍色背景的段落,因為相關的樣式成功被載入了。而另一個段落沒有樣式,因為其樣式缺少了 nonce 值。

csp-output.png

CSP header 還可以設定報告違規的 URL,你還可以在嚴格啟用 CSP 之前僅開啟報告模式,收集相關資料。

你可以在 MDN CSP 介紹檢視更多資訊,並瀏覽 “Can I Use” 網站檢視 CSP 相容性。大多數主流瀏覽器都支援這項特性。

總結

giphy.gif

可惜的是,在安全性方面不存在所謂的萬能方案,新的漏洞層出不窮。但是,你可以很輕鬆地在你的 web 應用中設定這些 HTTP header,顯著地提升你應用的安全性,何樂而不為呢?如果你想了解更多有關 HTTP header 提高安全性的最佳實踐,請瀏覽 securityheaders.io

如果你想了解更多 web 安全方面的最佳實踐,請訪問 Open Web Applications Security Project(OWASP),它涵蓋了廣泛的主題及有用的資源。

如果你有任何問題,或有其它用於提升 Node.js web 應用的工具,請隨時聯絡我:


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章