ExpressJS (TypeScript) – CORS 與安全性設定
主題:Helmet 基礎安全防護
簡介
在 Web 應用程式的開發過程中,安全性往往是最容易被忽略卻又最關鍵的環節。即使功能完整、效能優秀,若缺乏基本的防護機制,仍可能因 XSS、點擊劫持、MIME sniffing 等常見攻擊而遭受資安風險。
Node.js 生態系統提供了 helmet 這個輕量級的中介軟體套件,專門為 Express(或 Nest、Koa 等框架)自動設定一系列 HTTP Header,以降低上述威脅的可能性。
在 ExpressJS + TypeScript 的專案中,正確引入與配置 helmet,不僅能提升應用的安全基礎,還能與 TypeScript 的型別系統結合,讓開發者在編譯階段即捕捉錯誤、避免錯誤設定。本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現如何在 Express + TypeScript 專案中使用 Helmet 進行基礎安全防護。
核心概念
1. Helmet 是什麼?
helmet 是一組 Express 中介軟體(middleware),它會在回應(response)中加入或調整 HTTP Header,以防止常見的 Web 攻擊。
主要包含以下幾個子套件(可單獨開關):
| Header | 功能說明 | 常見攻擊類型 |
|---|---|---|
Content-Security-Policy (CSP) |
限制瀏覽器載入資源的來源 | XSS、資料注入 |
X-Frame-Options |
防止頁面被嵌入 <iframe> |
點擊劫持 (Clickjacking) |
X-Content-Type-Options |
禁止 MIME sniffing | MIME sniffing 攻擊 |
Strict-Transport-Security (HSTS) |
強制使用 HTTPS | 中間人攻擊 (MITM) |
Referrer-Policy |
控制 Referer 資訊的傳遞 | 隱私洩漏 |
X-XSS-Protection |
啟用瀏覽器內建 XSS 防護 (已被部分瀏覽器棄用) | XSS |
重點:Helmet 只是一層防護,仍需配合其他安全措施(如驗證、授權、輸入驗證)才能構築完整的防線。
2. 為什麼要在 TypeScript 中使用 Helmet?
- 型別安全:
@types/helmet提供完整的型別定義,開發時能即時得到 IDE 提示與編譯錯誤,避免錯誤的選項名稱或設定值。 - 模組化設定:透過 TypeScript 的介面(interface)可以自訂
helmet的選項,讓設定更具可讀性與維護性。 - 與其他中介軟體共存:在 TypeScript 中使用
app.use()時,編譯器會檢查傳入的中介軟體是否符合RequestHandler型別,減少因參數錯誤導致的執行時例外。
3. 安裝與基本使用
npm i helmet # 安裝核心套件
npm i -D @types/helmet # 安裝型別定義(開發環境)
在 src/app.ts(或 src/server.ts)中加入:
import express, { Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
const app = express();
// 1️⃣ 套用 Helmet 完整預設
app.use(helmet());
// 2️⃣ 若想自訂個別 Header,可使用物件參數
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdnjs.cloudflare.com'],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:'],
},
},
// 其他子套件可在此關閉或調整
frameguard: { action: 'deny' },
hsts: { maxAge: 31536000, includeSubDomains: true },
})
);
// 測試路由
app.get('/', (req: Request, res: Response) => {
res.send('Hello, Helmet with TypeScript!');
});
export default app;
說明:上述程式碼先以
app.use(helmet())套用 全部預設,接著再傳入自訂設定,會 覆寫 預設行為。若只想啟用部分功能,可分別引用helmet.xxx()。
程式碼範例
以下提供 5 個常見且實用的 Helmet 設定範例,每個範例皆附上說明與在 TypeScript 中的型別寫法。
範例 1️⃣:最小化 CSP(Content‑Security‑Policy)
import helmet from 'helmet';
// 只允許同源(self)與 CDN 之 script
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.jsdelivr.net'],
styleSrc: ["'self'", "'unsafe-inline'"], // 若使用 inline style 必須允許
imgSrc: ["'self'", 'data:'],
},
})
);
為什麼需要 CSP?
CSP 能告訴瀏覽器「只能從哪裡載入資源」,即使攻擊者成功注入惡意 <script>,瀏覽器仍會因違反政策而拋棄。
範例 2️⃣:防止點擊劫持(X‑Frame‑Options)
app.use(
helmet.frameguard({
action: 'sameorigin', // 只允許相同網域嵌入
// action: 'deny' // 完全禁止嵌入
})
);
說明:sameorigin 允許自己的頁面在 <iframe> 中顯示,適合多層子系統;若不需要任何嵌入,使用 deny 更安全。
範例 3️⃣:啟用 HSTS(Strict‑Transport‑Security)
app.use(
helmet.hsts({
maxAge: 60 * 60 * 24 * 365, // 1 年(秒)
includeSubDomains: true,
preload: true, // 若想提交至 Chrome HSTS preload list
})
);
注意:啟用 HSTS 後,所有 HTTP 請求將自動升級為 HTTPS。在開發環境(如 localhost)不建議開啟,可利用 process.env.NODE_ENV 判斷。
範例 4️⃣:禁用 MIME Sniffing(X‑Content‑Type‑Options)
app.use(helmet.noSniff()); // 會在回應加入 X-Content-Type-Options: nosniff
說明:此 Header 告訴瀏覽器不要自行猜測檔案類型,避免惡意腳本被偽裝成圖片或 CSS 之類的檔案。
範例 5️⃣:自訂 Referrer‑Policy
app.use(
helmet.referrerPolicy({
policy: 'no-referrer-when-downgrade', // 預設值,可根據需求調整
// 常見選項:'no-referrer'、'origin'、'strict-origin-when-cross-origin'
})
);
為什麼要設定 Referrer‑Policy?
它能控制瀏覽器在發送請求時,是否攜帶前一頁的 URL。若放出太多資訊,可能洩漏內部路徑或機密參數。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方式 |
|---|---|---|
| 在開發環境直接套用 HSTS | 一旦設定 maxAge 大於 0,瀏覽器會將 localhost 記錄為 HTTPS,之後必須手動清除 HSTS 記錄才能回到 HTTP。 |
使用 if (process.env.NODE_ENV === 'production') 包裹 helmet.hsts(),或在本機設定 maxAge: 0。 |
CSP 設定過於寬鬆(如 script-src *) |
CSP 失去防護意義,仍會受到 XSS 攻擊。 | 僅允許可信來源,盡量避免 unsafe-inline 與 unsafe-eval。 |
忘記在 express.json() 前使用 Helmet |
若先解析 JSON,再套用 Helmet,有可能因錯誤的 Content-Type 產生安全漏洞。 |
建議 先 app.use(helmet()),再 app.use(express.json())。 |
同時使用多個相同功能的中介軟體(例如自行設定 X-Frame-Options) |
產生 Header 重複或衝突,導致瀏覽器忽略或回傳錯誤。 | 讓 Helmet 統一管理相關 Header,除非確實需要覆寫,否則不要自行再設定。 |
忘記在測試環境關閉 CSP 的 reportOnly |
開發時因 CSP 阻擋資源而無法正常測試。 | 可使用 helmet.contentSecurityPolicy({ reportOnly: true }) 先觀察報告,再正式啟用。 |
最佳實踐
- 分環境設定:在
src/config/security.ts中統一管理 Helmet 設定,依process.env.NODE_ENV切換不同的值。 - 結合
express-rate-limit:防止暴力破解與 DoS 攻擊,兩者相輔相成。 - 使用
helmet.hidePoweredBy():隱藏框架資訊(X-Powered-By: Express),降低資訊洩漏。 - 定期審查 CSP 報告:使用
report-uri或report-to讓瀏覽器把違規請求回報給你的端點,持續優化政策。
實際應用場景
場景 1:企業內部 API 服務
公司內部的微服務需要 嚴格的 HTTPS 與 HSTS,避免員工在不安全的 Wi‑Fi 上被中間人攻擊。
if (process.env.NODE_ENV === 'production') {
app.use(
helmet({
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
noSniff: true,
referrerPolicy: { policy: 'no-referrer' },
})
);
}
同時結合 express-rate-limit 限制每分鐘請求上限,提升整體安全性。
場景 2:公開的前端單頁應用(SPA)
SPA 需要載入外部 CDN(如 Bootstrap、Font Awesome),但不想讓任意第三方腳本執行。
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://cdnjs.cloudflare.com'],
styleSrc: ["'self'", 'https://fonts.googleapis.com', "'unsafe-inline'"],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
imgSrc: ["'self'", 'data:'],
},
reportOnly: false,
})
);
若 CSP 阻擋了合法資源,可先開啟 reportOnly 觀察報告,再正式啟用。
場景 3:多租戶 SaaS 平台
平台允許客戶自訂子域名(customer1.example.com),必須 限制子域名間的資源共享。
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req) => `'https://${req.hostname}'`],
// 每個租戶只能載入自己子域名的腳本
},
},
frameguard: { action: 'sameorigin' }, // 不允許跨租戶嵌入
})
);
透過函式型 scriptSrc,依請求的 hostname 動態產生允許的來源。
總結
- Helmet 是在 Express(含 TypeScript)環境中快速提升 HTTP 層安全的首選工具。
- 透過 型別定義,我們可以在編譯階段即捕捉錯誤,確保設定正確且不會因拼寫錯誤產生意外。
- CSP、HSTS、X‑Frame‑Options、X‑Content‑Type‑Options、Referrer‑Policy 等子套件,各自針對不同攻擊向量提供防護,建議在正式環境一次全部啟用,開發環境則依需求調整。
- 分環境管理、結合速率限制、定期檢視 CSP 報告 是落實安全的關鍵步驟。
只要在專案的入口檔(app.ts / server.ts)中加入適當的 helmet 設定,您就已經為 Express 應用鋪設了一層堅實的防護牆。未來隨著需求變化,只需要微調 helmet 的選項或加入自訂 Header,即可持續維護安全水準。祝您開發順利,應用安全無虞!