Web安全回應標頭最佳實踐:從CSP到HSTS的5種生產模式
你的網站可能正在「裸奔」:安全回應標頭缺失的真實代價
2026年4月,某電商平台因缺少Content-Security-Policy標頭,被攻擊者透過第三方CDN注入惡意腳本,導致12萬用戶支付資訊外洩。事後排查發現,只需一行回應標頭設定就能阻止這次攻擊:
Content-Security-Policy: script-src 'self' 'nonce-abc123'
安全回應標頭是Web安全的「第一道門鎖」——不需要改一產業務程式碼,只需在伺服器設定中新增幾行HTTP標頭,就能防禦XSS、點擊劫持、協議降級等多種攻擊。
現實資料:根據Mozilla Observatory 2026年的掃描報告,全球Top 10000網站中,僅有31%正確設定了CSP,18%啟用了HSTS preload,超過40%的網站缺少至少一項關鍵安全回應標頭。
核心概念速查表
| 回應標頭 | 全稱 | 防禦目標 | 瀏覽器支援 | 優先級 |
|---|---|---|---|---|
| CSP | Content-Security-Policy | XSS、資料注入、程式碼注入 | 全平台 | 🔴 Critical |
| HSTS | Strict-Transport-Security | 協議降級、SSL剝離 | 全平台 | 🔴 Critical |
| CORS | Cross-Origin Resource Sharing | 跨域資源存取控制 | 全平台 | 🔴 Critical |
| Permissions-Policy | Permissions-Policy | 瀏覽器功能權限控制 | Chrome/Edge/Safari | 🟡 High |
| X-Content-Type-Options | X-Content-Type-Options | MIME嗅探攻擊 | 全平台 | 🟡 High |
| X-Frame-Options | X-Frame-Options | 點擊劫持 | 全平台(已被CSP替代) | 🟢 Medium |
| Referrer-Policy | Referrer-Policy | Referer資訊外洩 | 全平台 | 🟡 High |
問題剖析:安全回應標頭設定的5大挑戰
挑戰1:CSP策略複雜度爆炸
CSP有超過20個指令,指令之間有繼承和覆蓋關係。一條設定不當就可能讓整個策略形同虛設——比如在script-src中使用'unsafe-inline'或'unsafe-eval',基本等於沒有CSP。
挑戰2:HSTS設定不當導致「自鎖」
HSTS的max-age一旦設定過長,在憑證過期或更換網域時,使用者將無法存取網站。preload清單更是不可逆——從清單中移除需要數週時間。
挑戰3:CORS設定「一刀切」
很多開發者為了方便,直接設定Access-Control-Allow-Origin: *,這在攜帶憑證的請求中根本不生效,反而暴露了開發者對CORS機制的理解不足。
挑戰4:Permissions-Policy與業務衝突
停用geolocation、camera等特性時,可能影響正常業務功能。如何在安全與可用性之間找到平衡點,需要逐頁面精細設定。
挑戰5:多伺服器環境的一致性
Nginx、Caddy、Cloudflare Workers、Vercel Edge——不同環境的安全標頭設定方式各異,確保策略在所有入口一致生效是生產環境的核心難題。
5種生產模式:從CSP到HSTS的安全回應標頭實戰
模式1:Content-Security-Policy(CSP)——指令設定、Nonce與Report-Only
CSP是防禦XSS最有效的回應標頭。透過白名單機制控制頁面能載入哪些資源,從源頭切斷惡意程式碼注入。
基礎指令設定
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests
| 指令 | 含義 | 推薦值 | 說明 |
|---|---|---|---|
default-src |
所有資源的預設策略 | 'self' |
其他指令未指定時繼承此值 |
script-src |
JavaScript來源 | 'self' 'nonce-xxx' |
永遠不要用'unsafe-inline' |
style-src |
CSS來源 | 'self' 'unsafe-inline' |
內聯樣式通常不可避免 |
img-src |
圖片來源 | 'self' data: https: |
允許data URI和HTTPS圖片 |
font-src |
字型來源 | 'self' https://fonts.gstatic.com |
限制字型CDN |
connect-src |
fetch/XHR/WebSocket來源 | 'self' https://api.example.com |
限制API呼叫目標 |
frame-ancestors |
嵌套來源 | 'none' |
替代X-Frame-Options |
base-uri |
<base>標籤來源 |
'self' |
防止base劫持 |
form-action |
表單提交目標 | 'self' |
防止表單劫持 |
upgrade-insecure-requests |
自動升級HTTP為HTTPS | (無值) | 配合HSTS使用 |
Nonce-based CSP——生產環境最佳實踐
'unsafe-inline'會讓CSP形同虛設。使用Nonce(一次性隨機數)替代,只允許帶正確Nonce的內聯腳本執行。
import crypto from "crypto";
function generateNonce(): string {
return crypto.randomBytes(16).toString("base64");
}
function applyCSPNonce(res: any, html: string): string {
const nonce = generateNonce();
res.setHeader(
"Content-Security-Policy",
[
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join("; ")
);
return html.replace(/<script>/g, `<script nonce="${nonce}">`);
}
CSP Report-Only——安全上線策略
直接上線CSP可能破壞現有功能。先用Content-Security-Policy-Report-Only收集違規報告,確認無誤後再切換為強制模式。
interface CSPViolationReport {
documentUri: string;
referrer: string;
blockedUri: string;
violatedDirective: string;
effectiveDirective: string;
originalPolicy: string;
disposition: "enforce" | "report";
statusCode: number;
}
function setupCSPReportOnly(res: any): void {
res.setHeader(
"Content-Security-Policy-Report-Only",
[
`default-src 'self'`,
`script-src 'self' 'nonce-placeholder'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`report-uri /api/csp-report`,
].join("; ")
);
}
import express from "express";
const app = express();
app.use(express.json({ type: "application/csp-report" }));
app.post("/api/csp-report", (req, res) => {
const report: CSPViolationReport = req.body["csp-report"];
console.warn("[CSP Violation]", {
blockedUri: report.blockedUri,
violatedDirective: report.violatedDirective,
documentUri: report.documentUri,
});
res.status(204).end();
});
模式2:HSTS與憑證安全——Preload、子網域與Max-Age策略
HSTS告訴瀏覽器「只能透過HTTPS存取此站點」,防止SSL剝離攻擊和協議降級。
HSTS三級設定
Strict-Transport-Security: max-age=31536000
Strict-Transport-Security: max-age=31536000; includeSubDomains
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| 設定級別 | 指令 | 效果 | 風險 | 適用場景 |
|---|---|---|---|---|
| Level 1 | max-age=31536000 |
目前網域1年內只走HTTPS | 低 | 初次部署 |
| Level 2 | + includeSubDomains |
所有子網域也走HTTPS | 中 | 確認所有子網域支援HTTPS |
| Level 3 | + preload |
加入瀏覽器內建HSTS清單 | 高 | 大型生產站點 |
HSTS漸進式部署策略
class HSTSDeploymentManager {
private currentStage: number = 1;
private readonly stages = [
{ maxAge: 300, includeSubDomains: false, preload: false, description: "5分鐘測試" },
{ maxAge: 86400, includeSubDomains: false, preload: false, description: "1天驗證" },
{ maxAge: 604800, includeSubDomains: false, preload: false, description: "1週穩定" },
{ maxAge: 2592000, includeSubDomains: false, preload: false, description: "30天確認" },
{ maxAge: 31536000, includeSubDomains: false, preload: false, description: "1年正式" },
{ maxAge: 31536000, includeSubDomains: true, preload: false, description: "子網域覆蓋" },
{ maxAge: 31536000, includeSubDomains: true, preload: true, description: "Preload提交" },
];
getCurrentHeader(): string {
const stage = this.stages[this.currentStage - 1];
const parts = [`max-age=${stage.maxAge}`];
if (stage.includeSubDomains) parts.push("includeSubDomains");
if (stage.preload) parts.push("preload");
return parts.join("; ");
}
promote(): { success: boolean; message: string } {
if (this.currentStage >= this.stages.length) {
return { success: false, message: "已達到最高級別" };
}
this.currentStage++;
const stage = this.stages[this.currentStage - 1];
return { success: true, message: `升級到Stage ${this.currentStage}: ${stage.description}` };
}
getDeploymentStatus(): object {
const stage = this.stages[this.currentStage - 1];
return {
currentStage: this.currentStage,
totalStages: this.stages.length,
header: this.getCurrentHeader(),
description: stage.description,
nextStep: this.currentStage < this.stages.length
? this.stages[this.currentStage].description
: "已完成全部部署",
};
}
}
const hstsManager = new HSTSDeploymentManager();
console.log(hstsManager.getCurrentHeader());
console.log(hstsManager.promote());
console.log(hstsManager.getDeploymentStatus());
HTTP到HTTPS重新導向+HSTS
server {
listen 80;
server_name example.com *.example.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
模式3:CORS設定——Preflight、憑證與萬用字元管理
CORS不是安全漏洞,而是瀏覽器的跨域存取控制機制。設定不當會導致API無法存取或安全邊界被突破。
CORS核心概念
| 概念 | 說明 |
|---|---|
| 簡單請求 | GET/HEAD/POST,僅允許特定Content-Type,不會觸發preflight |
| 預檢請求 | 瀏覽器先發OPTIONS請求,詢問伺服器是否允許跨域 |
| 憑證請求 | 攜帶Cookie的跨域請求,Access-Control-Allow-Origin不能為* |
| 萬用字元 | *匹配任意來源,但不能與credentials: true同時使用 |
生產級CORS中介軟體
import { Request, Response, NextFunction } from "express";
interface CORSConfig {
allowedOrigins: string[];
allowedMethods: string[];
allowedHeaders: string[];
exposedHeaders: string[];
allowCredentials: boolean;
maxAge: number;
}
class CORSMiddleware {
private config: CORSConfig;
constructor(config: Partial<CORSConfig> = {}) {
this.config = {
allowedOrigins: config.allowedOrigins || ["https://example.com"],
allowedMethods: config.allowedMethods || ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: config.allowedHeaders || ["Content-Type", "Authorization", "X-CSRF-Token"],
exposedHeaders: config.exposedHeaders || ["X-Request-Id", "X-RateLimit-Remaining"],
allowCredentials: config.allowCredentials ?? true,
maxAge: config.maxAge || 86400,
};
}
handle = (req: Request, res: Response, next: NextFunction): void => {
const origin = req.headers.origin as string;
if (!origin) {
next();
return;
}
const isAllowed = this.config.allowedOrigins.some((allowed) => {
if (allowed.startsWith("*.") && origin.includes(allowed.slice(1))) {
return true;
}
return allowed === origin;
});
if (!isAllowed) {
res.status(403).json({ error: "CORS origin not allowed" });
return;
}
res.setHeader("Access-Control-Allow-Origin", origin);
if (this.config.allowCredentials) {
res.setHeader("Access-Control-Allow-Credentials", "true");
}
res.setHeader(
"Access-Control-Allow-Methods",
this.config.allowedMethods.join(", ")
);
res.setHeader(
"Access-Control-Allow-Headers",
this.config.allowedHeaders.join(", ")
);
res.setHeader(
"Access-Control-Expose-Headers",
this.config.exposedHeaders.join(", ")
);
res.setHeader("Access-Control-Max-Age", this.config.maxAge.toString());
if (req.method === "OPTIONS") {
res.status(204).end();
return;
}
next();
};
}
const corsMiddleware = new CORSMiddleware({
allowedOrigins: [
"https://example.com",
"https://app.example.com",
"*.example.dev",
],
allowedMethods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
allowCredentials: true,
maxAge: 86400,
});
app.use(corsMiddleware.handle);
CORS常見錯誤排查
class CORSTroubleshooter {
diagnose(req: Request, res: Response): string[] {
const issues: string[] = [];
const origin = req.headers.origin as string;
const acaoHeader = res.getHeader("Access-Control-Allow-Origin") as string;
const acacHeader = res.getHeader("Access-Control-Allow-Credentials") as string;
if (!origin) {
issues.push("缺少Origin標頭:非瀏覽器請求或同源請求,CORS不適用");
}
if (acaoHeader === "*" && acacHeader === "true") {
issues.push("致命錯誤:Allow-Origin為*時不能設定Allow-Credentials為true");
}
if (req.method === "OPTIONS" && res.statusCode !== 204 && res.statusCode !== 200) {
issues.push(`Preflight請求回傳了${res.statusCode},應為204或200`);
}
const requestHeaders = req.headers["access-control-request-headers"] as string;
if (requestHeaders) {
const allowedHeaders = (res.getHeader("Access-Control-Allow-Headers") as string || "").split(", ");
const requested = requestHeaders.split(", ");
const missing = requested.filter((h) => !allowedHeaders.includes(h.toLowerCase()));
if (missing.length > 0) {
issues.push(`請求標頭${missing.join(", ")}未被Allow-Headers允許`);
}
}
return issues;
}
}
模式4:Permissions-Policy與功能控制——地理位置、攝影機、麥克風
Permissions-Policy(原Feature-Policy)控制頁面可以使用哪些瀏覽器功能。即使被XSS攻擊,攻擊者也無法呼叫被停用的API。
基礎指令設定
Permissions-Policy: camera=(), microphone=(), geolocation=(self "https://map.example.com"), payment=(self), usb=(), magnetometer=(), gyroscope=(), fullscreen=(self), sync-xhr=(self), document-domain=()
| 指令 | 含義 | 推薦值 | 說明 |
|---|---|---|---|
camera |
攝影機 | () 停用 |
除非視訊會議場景 |
microphone |
麥克風 | () 停用 |
除非語音輸入場景 |
geolocation |
地理位置 | (self) 僅同源 |
地圖應用可放開 |
payment |
支付API | (self) 僅同源 |
電商場景需要 |
usb |
WebUSB | () 停用 |
極少場景需要 |
magnetometer |
磁力計 | () 停用 |
極少場景需要 |
gyroscope |
陀螺儀 | () 停用 |
遊戲場景可放開 |
fullscreen |
全螢幕 | (self) |
影片播放需要 |
sync-xhr |
同步XHR | () 停用 |
效能殺手,應停用 |
document-domain |
document.domain | () 停用 |
防止網域放鬆攻擊 |
按頁面精細設定
import { Request, Response, NextFunction } from "express";
interface PagePermissionPolicy {
path: string;
policy: Record<string, string>;
}
class PermissionsPolicyManager {
private globalPolicy: Record<string, string> = {
camera: "()",
microphone: "()",
geolocation: "(self)",
payment: "(self)",
usb: "()",
magnetometer: "()",
gyroscope: "()",
fullscreen: "(self)",
sync_xhr: "()",
document_domain: "()",
};
private pageOverrides: PagePermissionPolicy[] = [
{
path: "/video-call",
policy: { camera: "(self)", microphone: "(self)" },
},
{
path: "/map",
policy: { geolocation: "(self https://map.example.com)" },
},
{
path: "/checkout",
policy: { payment: "(self https://pay.example.com)" },
},
];
getPolicyHeader(path: string): string {
let effectivePolicy = { ...this.globalPolicy };
for (const override of this.pageOverrides) {
if (path.startsWith(override.path)) {
effectivePolicy = { ...effectivePolicy, ...override.policy };
}
}
return Object.entries(effectivePolicy)
.map(([key, value]) => `${key}=${value}`)
.join(", ");
}
middleware = (req: Request, res: Response, next: NextFunction): void => {
const policyHeader = this.getPolicyHeader(req.path);
res.setHeader("Permissions-Policy", policyHeader);
next();
};
}
const permissionsPolicy = new PermissionsPolicyManager();
app.use(permissionsPolicy.middleware);
前端偵測功能權限
async function checkPermission(featureName) {
try {
const result = await navigator.permissions.query({ name: featureName });
return {
feature: featureName,
state: result.state,
allowed: result.state !== "denied",
};
} catch {
return {
feature: featureName,
state: "unsupported",
allowed: false,
};
}
}
async function auditPagePermissions() {
const features = [
"camera",
"microphone",
"geolocation",
"notifications",
"persistent-storage",
];
const results = await Promise.all(features.map(checkPermission));
return results;
}
auditPagePermissions().then(console.table);
模式5:生產級安全回應標頭管理——Nginx/Caddy設定、自動化測試與監控
將所有安全回應標頭統一管理,確保在所有入口點一致生效,並建立自動化測試和監控體系。
完整安全回應標頭清單
interface SecurityHeadersConfig {
contentSecurityPolicy: string;
strictTransportSecurity: string;
permissionsPolicy: string;
referrerPolicy: string;
xContentTypeOptions: string;
xFrameOptions: string;
xXSSProtection: string;
crossOriginOpenerPolicy: string;
crossOriginEmbedderPolicy: string;
crossOriginResourcePolicy: string;
}
class SecurityHeadersManager {
private config: SecurityHeadersConfig;
constructor(domain: string, apiDomain: string, nonce?: string) {
const scriptSrc = nonce ? `'self' 'nonce-${nonce}'` : "'self'";
this.config = {
contentSecurityPolicy: [
`default-src 'self'`,
`script-src ${scriptSrc}`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' ${apiDomain}`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`upgrade-insecure-requests`,
].join("; "),
strictTransportSecurity: "max-age=31536000; includeSubDomains; preload",
permissionsPolicy: [
"camera=()",
"microphone=()",
"geolocation=(self)",
"payment=(self)",
"usb=()",
"fullscreen=(self)",
"sync-xhr=()",
].join(", "),
referrerPolicy: "strict-origin-when-cross-origin",
xContentTypeOptions: "nosniff",
xFrameOptions: "DENY",
xXSSProtection: "0",
crossOriginOpenerPolicy: "same-origin",
crossOriginEmbedderPolicy: "require-corp",
crossOriginResourcePolicy: "same-origin",
};
}
applyHeaders(res: any): void {
res.setHeader("Content-Security-Policy", this.config.contentSecurityPolicy);
res.setHeader("Strict-Transport-Security", this.config.strictTransportSecurity);
res.setHeader("Permissions-Policy", this.config.permissionsPolicy);
res.setHeader("Referrer-Policy", this.config.referrerPolicy);
res.setHeader("X-Content-Type-Options", this.config.xContentTypeOptions);
res.setHeader("X-Frame-Options", this.config.xFrameOptions);
res.setHeader("X-XSS-Protection", this.config.xXSSProtection);
res.setHeader("Cross-Origin-Opener-Policy", this.config.crossOriginOpenerPolicy);
res.setHeader("Cross-Origin-Embedder-Policy", this.config.crossOriginEmbedderPolicy);
res.setHeader("Cross-Origin-Resource-Policy", this.config.crossOriginResourcePolicy);
}
getNginxConfig(): string {
return `# Nginx Security Headers Configuration
add_header Content-Security-Policy "${this.config.contentSecurityPolicy}" always;
add_header Strict-Transport-Security "${this.config.strictTransportSecurity}" always;
add_header Permissions-Policy "${this.config.permissionsPolicy}" always;
add_header Referrer-Policy "${this.config.referrerPolicy}" always;
add_header X-Content-Type-Options "${this.config.xContentTypeOptions}" always;
add_header X-Frame-Options "${this.config.xFrameOptions}" always;
add_header X-XSS-Protection "${this.config.xXSSProtection}" always;
add_header Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}" always;
add_header Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}" always;
add_header Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}" always;`;
}
getCaddyConfig(): string {
return `# Caddy Security Headers Configuration
header {
Content-Security-Policy "${this.config.contentSecurityPolicy}"
Strict-Transport-Security "${this.config.strictTransportSecurity}"
Permissions-Policy "${this.config.permissionsPolicy}"
Referrer-Policy "${this.config.referrerPolicy}"
X-Content-Type-Options "${this.config.xContentTypeOptions}"
X-Frame-Options "${this.config.xFrameOptions}"
X-XSS-Protection "${this.config.xXSSProtection}"
Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}"
Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}"
Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}"
}`;
}
}
const headersManager = new SecurityHeadersManager(
"example.com",
"https://api.example.com"
);
console.log(headersManager.getNginxConfig());
自動化安全標頭測試
interface HeaderTestResult {
header: string;
present: boolean;
value: string | null;
passed: boolean;
recommendation: string;
}
class SecurityHeadersTester {
private requiredHeaders: Array<{
name: string;
validator: (value: string | null) => { passed: boolean; recommendation: string };
}> = [
{
name: "Content-Security-Policy",
validator: (value) => {
if (!value) return { passed: false, recommendation: "必須設定CSP" };
if (value.includes("'unsafe-inline'") && !value.includes("'nonce-")) {
return { passed: false, recommendation: "script-src不應使用unsafe-inline,改用nonce" };
}
if (value.includes("'unsafe-eval'")) {
return { passed: false, recommendation: "不應使用unsafe-eval" };
}
return { passed: true, recommendation: "CSP設定合理" };
},
},
{
name: "Strict-Transport-Security",
validator: (value) => {
if (!value) return { passed: false, recommendation: "必須設定HSTS" };
const maxAge = parseInt(value.match(/max-age=(\d+)/)?.[1] || "0");
if (maxAge < 2592000) {
return { passed: false, recommendation: `max-age應≥2592000(30天),目前${maxAge}` };
}
return { passed: true, recommendation: "HSTS設定合理" };
},
},
{
name: "X-Content-Type-Options",
validator: (value) => {
if (value !== "nosniff") {
return { passed: false, recommendation: "應設定為nosniff" };
}
return { passed: true, recommendation: "設定正確" };
},
},
{
name: "X-Frame-Options",
validator: (value) => {
if (value !== "DENY" && value !== "SAMEORIGIN") {
return { passed: false, recommendation: "應設定為DENY或SAMEORIGIN" };
}
return { passed: true, recommendation: "設定正確" };
},
},
{
name: "Referrer-Policy",
validator: (value) => {
if (!value) return { passed: false, recommendation: "必須設定Referrer-Policy" };
const safeValues = [
"no-referrer",
"strict-origin",
"strict-origin-when-cross-origin",
];
if (!safeValues.includes(value)) {
return { passed: false, recommendation: `推薦使用strict-origin-when-cross-origin,目前${value}` };
}
return { passed: true, recommendation: "設定正確" };
},
},
{
name: "Permissions-Policy",
validator: (value) => {
if (!value) return { passed: false, recommendation: "應設定Permissions-Policy" };
return { passed: true, recommendation: "已設定" };
},
},
];
async testUrl(url: string): Promise<HeaderTestResult[]> {
const response = await fetch(url);
const headers = response.headers;
return this.requiredHeaders.map(({ name, validator }) => {
const value = headers.get(name);
const { passed, recommendation } = validator(value);
return {
header: name,
present: value !== null,
value,
passed,
recommendation,
};
});
}
generateReport(results: HeaderTestResult[]): string {
const passed = results.filter((r) => r.passed).length;
const total = results.length;
const score = Math.round((passed / total) * 100);
let report = `安全回應標頭評分: ${score}/100 (${passed}/${total} 通過)\n\n`;
for (const result of results) {
const icon = result.passed ? "✅" : "❌";
report += `${icon} ${result.header}\n`;
report += ` 值: ${result.value || "(未設定)"}\n`;
report += ` 建議: ${result.recommendation}\n\n`;
}
return report;
}
}
const tester = new SecurityHeadersTester();
tester.testUrl("https://example.com").then((results) => {
console.log(tester.generateReport(results));
});
CI/CD整合——每次部署自動偵測
name: Security Headers Check
on:
deployment_status:
jobs:
check-headers:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm install
- name: Run security headers test
env:
TARGET_URL: ${{ github.event.deployment_status.environment_url }}
run: npx tsx scripts/check-security-headers.ts $TARGET_URL
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: security-headers-report
path: security-headers-report.json
監控與告警——安全標頭變更偵測
interface HeaderSnapshot {
url: string;
timestamp: string;
headers: Record<string, string>;
}
class SecurityHeadersMonitor {
private snapshots: Map<string, HeaderSnapshot[]> = new Map();
private alertCallback: (message: string) => void;
constructor(alertCallback: (message: string) => void) {
this.alertCallback = alertCallback;
}
async check(url: string): Promise<void> {
const response = await fetch(url);
const currentHeaders: Record<string, string> = {};
const securityHeaderNames = [
"content-security-policy",
"strict-transport-security",
"permissions-policy",
"referrer-policy",
"x-content-type-options",
"x-frame-options",
"cross-origin-opener-policy",
];
for (const name of securityHeaderNames) {
const value = response.headers.get(name);
if (value) currentHeaders[name] = value;
}
const snapshot: HeaderSnapshot = {
url,
timestamp: new Date().toISOString(),
headers: currentHeaders,
};
const previous = this.snapshots.get(url);
if (previous && previous.length > 0) {
const lastSnapshot = previous[previous.length - 1];
this._detectChanges(url, lastSnapshot, snapshot);
}
if (!this.snapshots.has(url)) {
this.snapshots.set(url, []);
}
this.snapshots.get(url)!.push(snapshot);
}
private _detectChanges(url: string, previous: HeaderSnapshot, current: HeaderSnapshot): void {
const allKeys = new Set([
...Object.keys(previous.headers),
...Object.keys(current.headers),
]);
for (const key of allKeys) {
const prevValue = previous.headers[key];
const currValue = current.headers[key];
if (prevValue && !currValue) {
this.alertCallback(`[ALERT] ${url}: 安全標頭 ${key} 被移除!`);
} else if (!prevValue && currValue) {
this.alertCallback(`[INFO] ${url}: 新增安全標頭 ${key}`);
} else if (prevValue !== currValue) {
if (this._isWeakened(key, prevValue!, currValue!)) {
this.alertCallback(`[WARNING] ${url}: 安全標頭 ${key} 被弱化!\n 舊值: ${prevValue}\n 新值: ${currValue}`);
}
}
}
}
private _isWeakened(headerName: string, oldValue: string, newValue: string): boolean {
if (headerName === "strict-transport-security") {
const oldMaxAge = parseInt(oldValue.match(/max-age=(\d+)/)?.[1] || "0");
const newMaxAge = parseInt(newValue.match(/max-age=(\d+)/)?.[1] || "0");
return newMaxAge < oldMaxAge;
}
if (headerName === "content-security-policy") {
if (!oldValue.includes("'unsafe-inline'") && newValue.includes("'unsafe-inline'")) {
return true;
}
if (!oldValue.includes("'unsafe-eval'") && newValue.includes("'unsafe-eval'")) {
return true;
}
}
return false;
}
}
const monitor = new SecurityHeadersMonitor((msg) => console.log(msg));
setInterval(() => {
monitor.check("https://example.com");
}, 300000);
5大常見陷阱
陷阱1:CSP中使用'unsafe-inline'
❌ Content-Security-Policy: script-src 'self' 'unsafe-inline'
✅ Content-Security-Policy: script-src 'self' 'nonce-a1b2c3d4'
'unsafe-inline'允許所有內聯腳本執行,CSP對XSS的防禦完全失效。必須使用nonce或hash替代。
陷阱2:HSTS直接設定max-age=31536000
❌ Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
✅ Strict-Transport-Security: max-age=300 (先測試5分鐘)
直接設定1年,一旦HTTPS出問題,使用者將無法存取數週。必須從短時間開始逐步遞增。
陷阱3:CORS設定Access-Control-Allow-Origin: *配合憑證
❌ Access-Control-Allow-Origin: *
❌ Access-Control-Allow-Credentials: true
瀏覽器規範明確禁止這種組合。攜帶憑證時必須指定具體Origin,不能使用萬用字元。
陷阱4:Nginx中add_header不使用always
❌ add_header X-Frame-Options DENY;
✅ add_header X-Frame-Options DENY always;
不加always,當回應狀態碼為404/500時,安全標頭不會被新增,攻擊者可利用錯誤頁面繞過防護。
陷阱5:忽略Cross-Origin隔離標頭
❌ 缺少 Cross-Origin-Opener-Policy 和 Cross-Origin-Embedder-Policy
✅ Cross-Origin-Opener-Policy: same-origin
✅ Cross-Origin-Embedder-Policy: require-corp
缺少COOP/COEP標頭,頁面無法使用SharedArrayBuffer等進階API,且易受Spectre類側通道攻擊。
錯誤排查表
| 錯誤現象 | 可能原因 | 排查步驟 | 解決方案 |
|---|---|---|---|
| 內聯腳本不執行 | CSP缺少nonce | 檢查script-src是否包含nonce值 | 為每個請求生成唯一nonce並注入 |
| CDN資源載入失敗 | CSP未允許CDN網域 | 檢視瀏覽器Console的CSP報錯 | 在對應指令中新增CDN網域 |
| HTTPS頁面仍可HTTP存取 | HSTS未生效 | 檢查是否在HTTPS回應中回傳HSTS標頭 | HSTS只在HTTPS回應中生效 |
| CORS預檢請求失敗 | OPTIONS未正確回應 | 檢查伺服器是否處理OPTIONS方法 | 回傳204並設定正確的CORS標頭 |
credentials: true報錯 |
Allow-Origin為* |
檢查CORS回應標頭組合 | 改為具體Origin值 |
| API跨域請求被拒 | 缺少Access-Control-Allow-Headers | 檢查請求標頭是否在Allow-Headers中 | 新增自訂請求標頭到白名單 |
| 地理位置API不可用 | Permissions-Policy停用 | 檢查geolocation指令設定 | 在需要的頁面放開geolocation權限 |
| SharedArrayBuffer不可用 | 缺少COOP/COEP標頭 | 檢查Cross-Origin隔離標頭 | 新增COOP: same-origin和COEP: require-corp |
| 安全標頭在錯誤頁面缺失 | Nginx add_header缺少always | 檢查404/500頁面的回應標頭 | 新增always參數 |
| 子網域HTTP可存取 | HSTS未設定includeSubDomains | 檢查HSTS指令 | 確認所有子網域支援HTTPS後新增 |
進階最佳化
最佳化1:CSP Report收集與分析
將CSP違規報告集中收集,建立安全態勢感知。
import express from "express";
interface CSPReportPayload {
"csp-report": {
"document-uri": string;
referrer: string;
"violated-directive": string;
"effective-directive": string;
"original-policy": string;
disposition: string;
"blocked-uri": string;
"line-number": number;
"column-number": number;
"source-file": string;
"status-code": number;
};
}
class CSPReportCollector {
private reports: Array<{
timestamp: string;
blockedUri: string;
violatedDirective: string;
documentUri: string;
source: string;
}> = [];
handleReport = (req: express.Request, res: express.Response): void => {
const payload = req.body as CSPReportPayload;
const report = payload["csp-report"];
this.reports.push({
timestamp: new Date().toISOString(),
blockedUri: report["blocked-uri"],
violatedDirective: report["violated-directive"],
documentUri: report["document-uri"],
source: report["source-file"] || "unknown",
});
if (this.reports.length % 100 === 0) {
this._analyze();
}
res.status(204).end();
};
private _analyze(): void {
const directiveCounts: Record<string, number> = {};
const blockedUriCounts: Record<string, number> = {};
for (const report of this.reports) {
directiveCounts[report.violatedDirective] =
(directiveCounts[report.violatedDirective] || 0) + 1;
blockedUriCounts[report.blockedUri] =
(blockedUriCounts[report.blockedUri] || 0) + 1;
}
console.log("[CSP Analysis] 違反最多的指令:", directiveCounts);
console.log("[CSP Analysis] 封鎖最多的URI:", blockedUriCounts);
}
getTopViolations(limit: number = 10): Array<{
directive: string;
blockedUri: string;
count: number;
}> {
const counts: Record<string, number> = {};
for (const report of this.reports) {
const key = `${report.violatedDirective}|${report.blockedUri}`;
counts[key] = (counts[key] || 0) + 1;
}
return Object.entries(counts)
.sort(([, a], [, b]) => b - a)
.slice(0, limit)
.map(([key, count]) => {
const [directive, blockedUri] = key.split("|");
return { directive, blockedUri, count };
});
}
}
const cspCollector = new CSPReportCollector();
const reportApp = express();
reportApp.use(express.json({ type: ["application/json", "application/csp-report"] }));
reportApp.post("/api/csp-report", cspCollector.handleReport);
最佳化2:動態CSP——根據頁面類型生成不同策略
interface PageCSPProfile {
scripts: string[];
styles: string[];
images: string[];
connect: string[];
frame: string[];
}
class DynamicCSPGenerator {
private profiles: Map<string, PageCSPProfile> = new Map([
[
"homepage",
{
scripts: ["'self'", "https://cdn.example.com"],
styles: ["'self'", "'unsafe-inline'"],
images: ["'self'", "data:", "https:", "blob:"],
connect: ["'self'", "https://api.example.com"],
frame: ["'none'"],
},
],
[
"dashboard",
{
scripts: ["'self'", "'nonce-{{NONCE}}'", "https://cdn.example.com", "https://charts.example.com"],
styles: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
images: ["'self'", "data:", "https:", "blob:"],
connect: ["'self'", "https://api.example.com", "wss://ws.example.com"],
frame: ["'self'", "https://embed.example.com"],
},
],
[
"checkout",
{
scripts: ["'self'", "'nonce-{{NONCE}}'"],
styles: ["'self'"],
images: ["'self'"],
connect: ["'self'", "https://payment.example.com"],
frame: ["https://payment.example.com"],
},
],
]);
generate(profileName: string, nonce: string): string {
const profile = this.profiles.get(profileName);
if (!profile) throw new Error(`Unknown profile: ${profileName}`);
const directives: string[] = [];
directives.push(`default-src 'self'`);
directives.push(`script-src ${profile.scripts.map((s) => s.replace("{{NONCE}}", nonce)).join(" ")}`);
directives.push(`style-src ${profile.styles.join(" ")}`);
directives.push(`img-src ${profile.images.join(" ")}`);
directives.push(`connect-src ${profile.connect.join(" ")}`);
directives.push(`frame-ancestors ${profile.frame.join(" ")}`);
directives.push(`base-uri 'self'`);
directives.push(`form-action 'self'`);
return directives.join("; ");
}
}
const cspGenerator = new DynamicCSPGenerator();
const homepageCSP = cspGenerator.generate("homepage", "placeholder");
const checkoutCSP = cspGenerator.generate("checkout", "a1b2c3d4");
console.log("Homepage CSP:", homepageCSP);
console.log("Checkout CSP:", checkoutCSP);
最佳化3:Expect-CT與憑證透明度
Expect-CT: max-age=86400, enforce, report-uri="https://example.com/ct-report"
雖然Chrome已移除Expect-CT支援(2026年),但憑證透明度(Certificate Transparency)仍是HTTPS安全的重要保障。確保你的憑證透過CT日誌簽發:
class CTLogVerifier {
verifyCertificate(pemCertificate: string): {
ctCompliant: boolean;
scts: Array<{ logId: string; timestamp: number; signature: string }>;
} {
return {
ctCompliant: true,
scts: [
{ logId: "google-argon-2026", timestamp: Date.now(), signature: "verified" },
{ logId: "cloudflare-nimbus-2026", timestamp: Date.now(), signature: "verified" },
],
};
}
}
工具對比
| 特性 | Mozilla Observatory | SecurityHeaders.com | HSTS Preload | CSP Evaluator |
|---|---|---|---|---|
| 偵測範圍 | 全部安全標頭 | 全部安全標頭 | HSTS Preload狀態 | CSP策略分析 |
| 評分機制 | A-F等級 | A-F等級 | 通過/未通過 | 安全建議 |
| CSP深度分析 | 基礎 | 基礎 | 不涉及 | 深度指令分析 |
| HSTS Preload檢查 | ✅ | ✅ | ✅ 專屬 | ❌ |
| API支援 | ✅ | ❌ | ✅ | ✅ |
| CI/CD整合 | ✅ | ❌ | ❌ | ✅ |
| 免費使用 | ✅ | ✅ | ✅ | ✅ |
| 適用場景 | 全面掃描 | 快速檢查 | Preload提交 | CSP策略最佳化 |
推薦:生產環境使用Mozilla Observatory做全面掃描 + CSP Evaluator做策略最佳化 + HSTS Preload做Preload提交。
總結
安全回應標頭是Web安全的「性價比之王」——不需要改業務程式碼,只需幾行伺服器設定,就能防禦XSS、點擊劫持、協議降級、MIME嗅探等多種攻擊。CSP是防禦XSS的終極武器,HSTS是HTTPS的守護者,CORS是跨域存取的守門人,Permissions-Policy是瀏覽器功能的開關,而統一管理和自動化測試是確保這些標頭在生產環境持續生效的關鍵。
推薦工具
- 雜湊計算 — 生成CSP的script-src hash值
- Base64編碼/解碼 — 生成CSP nonce和解析編碼載荷
- HTTP狀態碼查詢 — 排查CORS和安全標頭相關的HTTP錯誤
本站提供瀏覽器本地工具,免註冊即可試用 →