本文 AI 產出,尚未審核

JavaScript 課程 – 安全與防護

主題:CSRF(跨站請求偽造)


簡介

在 Web 應用程式的開發過程中,安全往往是最容易被忽視卻又最關鍵的環節。
CSRF(Cross‑Site Request Forgery,跨站請求偽造)是一種常見且危險的攻擊手法,會在使用者已經登入的情況下,利用瀏覽器自動帶入的 Cookie,讓攻擊者「冒充」使用者對目標網站發送惡意請求。

如果不加防護,攻擊者可以在不知情的情況下,讓受害者的帳號完成如「刪除資料、轉帳、變更密碼」等敏感操作。
因此,了解 CSRF 的原理、如何在前端與後端協同防禦,是每位 JavaScript 開發者必備的基礎能力。


核心概念

1. CSRF 的運作流程

  1. 使用者在 A 站點登入,瀏覽器儲存了 sessionIdauthToken 等 Cookie。
  2. 使用者在同一個瀏覽器開啟另一個惡意網站 B
  3. B 網站嵌入一段 HTML(如 <img src="https://A.com/api/transfer?amount=1000&to=attacker">)或 JavaScript,自動向 A 站點發送請求
  4. 瀏覽器在發送請求時,會自動把 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 屬性為 StrictLax,阻止跨站點自動送出 Cookie。 確認後端已正確設定;前端可透過 document.cookie 檢查。
雙重驗證(Double Submit Cookie) 同時在 Cookie 與請求參數/標頭中送出相同的隨機值,伺服器比對兩者是否相符。 在 JavaScript 中讀取 Cookie,並在每次請求時加入自訂標頭。
檢查 Referer / Origin 伺服器驗證請求的 RefererOrigin 是否為自己的域名。 前端無需額外處理,只要確保請求會帶上正確的 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 必須是 LaxStrict;若必須 None,一定要加 Secure
只檢查 Referer 而不檢查 Origin 有些瀏覽器或代理會省略 Referer,導致誤判。 同時檢查 RefererOrigin,或直接採用 token 防禦。

其他實務建議

  1. 在部署環境使用 HTTPS:只有在安全通道下,SecureSameSite 才能發揮效用。
  2. 定期輪換 CSRF Token:每次渲染頁面或每個 Session 產生新 token,降低被竊取的風險。
  3. 在 API 文件中明確說明:前端開發者必須在所有寫入型 API 中加入 X‑CSRF‑TOKEN(或等效欄位)。
  4. 使用框架自帶的防護:如 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 防護不是「加一段程式碼」就能解決,而是前後端協作、全流程檢查的綜合策略。