JavaScript 課程 – 安全與防護
主題:CSRF(跨站請求偽造)
簡介
在 Web 應用程式的開發過程中,安全往往是最容易被忽視卻又最關鍵的環節。
CSRF(Cross‑Site Request Forgery,跨站請求偽造)是一種常見且危險的攻擊手法,會在使用者已經登入的情況下,利用瀏覽器自動帶入的 Cookie,讓攻擊者「冒充」使用者對目標網站發送惡意請求。
如果不加防護,攻擊者可以在不知情的情況下,讓受害者的帳號完成如「刪除資料、轉帳、變更密碼」等敏感操作。
因此,了解 CSRF 的原理、如何在前端與後端協同防禦,是每位 JavaScript 開發者必備的基礎能力。
核心概念
1. CSRF 的運作流程
- 使用者在 A 站點登入,瀏覽器儲存了
sessionId、authToken等 Cookie。 - 使用者在同一個瀏覽器開啟另一個惡意網站 B。
- B 網站嵌入一段 HTML(如
<img src="https://A.com/api/transfer?amount=1000&to=attacker">)或 JavaScript,自動向 A 站點發送請求。 - 瀏覽器在發送請求時,會自動把 A 站點的 Cookie 附上,伺服器誤以為是使用者本人的合法操作。
關鍵點:CSRF 攻擊不需要讀取或修改回應內容,只要「觸發」請求即可。
2. 為什麼純粹依賴 Cookie 不夠?
- 同源政策(Same‑Origin Policy)只限制 JavaScript 讀取跨域回應,不限制發送跨域請求。
- 因此,只要能讓瀏覽器發送請求(例如
<img>、<script>、<form>),攻擊就能成立。
3. 防禦機制概覽
| 防禦方式 | 原理 | 前端需要做什麼 |
|---|---|---|
| CSRF Token | 伺服器在每次渲染表單或回傳 API 時產生一次性 token,要求客戶端在請求中回傳相同 token。 | 在 AJAX、<form> 隱藏欄位或 fetch 標頭中加入 token。 |
| SameSite Cookie | 設定 Cookie 的 SameSite 屬性為 Strict 或 Lax,阻止跨站點自動送出 Cookie。 |
確認後端已正確設定;前端可透過 document.cookie 檢查。 |
| 雙重驗證(Double Submit Cookie) | 同時在 Cookie 與請求參數/標頭中送出相同的隨機值,伺服器比對兩者是否相符。 | 在 JavaScript 中讀取 Cookie,並在每次請求時加入自訂標頭。 |
| 檢查 Referer / Origin | 伺服器驗證請求的 Referer 或 Origin 是否為自己的域名。 |
前端無需額外處理,只要確保請求會帶上正確的 Origin(瀏覽器自動完成)。 |
程式碼範例
以下示範在 Node.js + Express 後端與 前端 JavaScript 中實作常見的 CSRF 防禦方式。每段程式碼均附有說明註解,方便初學者快速上手。
範例 1:使用 csurf 中介軟體產生 CSRF Token
// server.js(Node.js + Express)
const express = require('express');
const cookieParser = require('cookie-parser');
const csurf = require('csurf');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// 設定 csurf,使用 cookie 儲存 token
const csrfProtection = csurf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
// 把 token 注入到 HTML 表單中
res.send(`
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<input name="amount" placeholder="金額">
<button type="submit">送出</button>
</form>
`);
});
app.post('/process', csrfProtection, (req, res) => {
// 若 token 驗證失敗,會直接拋出 403
res.send('交易已完成!');
});
app.listen(3000, () => console.log('Server listening on http://localhost:3000'));
說明:
csurf會在每次請求時比對表單內的_csrf欄位與 Cookie 中的 token,若不符即視為 CSRF 攻擊。
範例 2:前端使用 fetch 搭配 CSRF Token
// client.js(瀏覽器端)
async function submitTransfer(amount) {
// 先從 cookie 讀取 token(csurf 預設放在 XSRF‑TOKEN)
const token = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 把 token 放在自訂標頭 X‑XSRF‑TOKEN
'X-XSRF-TOKEN': token
},
body: JSON.stringify({ amount })
});
const result = await response.text();
console.log(result);
}
// 呼叫範例
submitTransfer(500);
重點:前端必須把 token 放在非 Cookie 的位置(如自訂標頭),才能讓伺服器驗證。
範例 3:使用 SameSite Cookie 防禦(後端設定)
// server.js(Express)
app.use((req, res, next) => {
// 設定 session cookie 為 SameSite=Lax
res.cookie('sessionId', 'abc123', {
httpOnly: true,
sameSite: 'lax', // 或 'strict' 依需求調整
secure: true // 只在 HTTPS 傳輸
});
next();
});
說明:
SameSite=Lax會允許從同站點的 GET 請求攜帶 Cookie,但會阻止跨站點的 POST、PUT 等「有副作用」的請求。
範例 4:Double Submit Cookie 實作
// 產生隨機值並寫入兩個位置(Cookie + Header)
// server.js
app.get('/login', (req, res) => {
const token = crypto.randomBytes(24).toString('hex');
// 把 token 存入 cookie
res.cookie('csrfToken', token, { httpOnly: false });
// 回傳 HTML,前端會自行把 token 帶入 Header
res.send(`
<script src="/app.js"></script>
<h1>已登入</h1>
`);
});
// app.js(前端)
document.addEventListener('DOMContentLoaded', () => {
const csrfToken = document.cookie
.split('; ')
.find(c => c.startsWith('csrfToken='))
.split('=')[1];
// 所有 AJAX 請求自動加上 X‑CSRF‑TOKEN 標頭
const originalFetch = window.fetch;
window.fetch = (url, options = {}) => {
options.headers = {
...options.headers,
'X-CSRF-TOKEN': csrfToken
};
return originalFetch(url, options);
};
});
// server.js(驗證)
app.post('/api/update-profile', (req, res) => {
const cookieToken = req.cookies.csrfToken;
const headerToken = req.get('X-CSRF-TOKEN');
if (cookieToken !== headerToken) {
return res.status(403).send('CSRF validation failed');
}
// 正常處理請求…
res.send('Profile updated');
});
關鍵:即使攻擊者無法取得 Cookie 中的 token,若自行偽造同樣的值也無法通過驗證,因為兩個來源必須相同。
範例 5:檢查 Origin 標頭(僅適用於 AJAX)
// server.js(Express)
app.post('/api/delete', (req, res) => {
const origin = req.get('origin') || '';
if (origin !== 'https://mytrusteddomain.com') {
return res.status(403).send('Invalid origin');
}
// 執行刪除操作…
res.send('Deleted');
});
說明:
Origin標頭只會在跨域的 CORS 請求或fetch/XMLHttpRequest中自動帶出,無法被普通的<img>或<form>請求偽造,適合作為第二道防線。
常見陷阱與最佳實踐
| 常見陷阱 | 為何會發生 | 建議的最佳實踐 |
|---|---|---|
| 只在 GET 請求加 Token | 攻擊者常利用 POST、PUT、DELETE 等有副作用的請求。 | 所有修改資料的 HTTP 方法(POST、PUT、PATCH、DELETE)皆必須驗證 CSRF token。 |
| 把 token 放在 URL 查詢字串 | URL 會被瀏覽器快取、紀錄,甚至洩漏到 Referer。 | 使用隱藏欄位或自訂標頭,避免在 URL 中傳遞。 |
| 忘記在 AJAX 請求中帶 token | 前端開發者習慣使用 fetch,但未自動加入 token。 |
在全域攔截器(如 axios.interceptors)或覆寫 fetch,統一加入 token。 |
SameSite 設為 None 且未加 Secure |
會使 Cookie 在所有情況下都被送出,且在 HTTP 上暴露。 | SameSite 必須是 Lax 或 Strict;若必須 None,一定要加 Secure。 |
| 只檢查 Referer 而不檢查 Origin | 有些瀏覽器或代理會省略 Referer,導致誤判。 | 同時檢查 Referer 與 Origin,或直接採用 token 防禦。 |
其他實務建議
- 在部署環境使用 HTTPS:只有在安全通道下,
Secure與SameSite才能發揮效用。 - 定期輪換 CSRF Token:每次渲染頁面或每個 Session 產生新 token,降低被竊取的風險。
- 在 API 文件中明確說明:前端開發者必須在所有寫入型 API 中加入
X‑CSRF‑TOKEN(或等效欄位)。 - 使用框架自帶的防護:如 Laravel、Django、Rails 都內建 CSRF 機制,盡量直接使用。
實際應用場景
| 場景 | 可能的 CSRF 攻擊方式 | 防護措施 |
|---|---|---|
| 線上銀行轉帳 | 攻擊者造一個 <form>,自動提交 POST /transfer,金額與收款人寫死。 |
使用 CSRF Token,且在轉帳頁面加入一次性驗證碼(OTP)作雙重防護。 |
| 社交平台貼文刪除 | 惡意網站嵌入 <img src="https://social.com/api/deletePost?id=123">,觸發刪除。 |
SameSite=Lax/Strict Cookie + Token 驗證。 |
| 電商結帳 | 攻擊者透過 <script> 發送 POST /order,把受害者的付款資訊填入。 |
Double Submit Cookie + CSP(Content‑Security‑Policy)限制外部腳本。 |
| 單頁應用(SPA)使用 AJAX | 攻擊者利用 XSS 注入腳本,直接呼叫 API。 | XSS 防護(輸入過濾、CSP) + CSRF Token(自訂標頭)。 |
| 跨域 API(CORS) | 只要 CORS 設定寬鬆,攻擊者可用 fetch 發送跨域請求。 |
CORS 嚴格白名單 + Origin 檢查 + CSRF Token。 |
總結
CSRF 是利用瀏覽器自動帶入認證資訊的特性,對任何依賴 Cookie 認證的 Web 應用都構成威脅。
透過 CSRF Token、SameSite Cookie、Double Submit Cookie 以及 Origin/Referer 檢查,我們可以在前端與後端建立多層防禦,將攻擊成功的機率降至最低。
在實務開發中,不要只依賴單一機制,而是將多種防護手段結合:
- 後端產生一次性 token,前端在所有寫入型請求中以自訂標頭送出;
- Cookie 設為
SameSite=Lax(或Strict)並加上Secure; - 針對重要操作再加上 OTP 或二次驗證。
只要遵循以上最佳實踐,從開發階段就把 CSRF 的風險降到最低,才能為使用者提供更安全、可靠的 Web 體驗。
關鍵一句話:CSRF 防護不是「加一段程式碼」就能解決,而是前後端協作、全流程檢查的綜合策略。