確認表單提交來源為本站,是防禦 CSRF (Cross-Site Request Forgery) 攻擊的基礎措施。本文比較三種驗證策略:HTTP_REFERER 檢查、隱藏 Token 驗證、SameSite Cookie 屬性,分析各自優缺點與適用場景,提供生產環境可直接部署的完整實作範例。
| 策略 | 原理 | 安全性 | 相容性 | 適用場景 |
|---|---|---|---|---|
| HTTP_REFERER | 檢查 Header 來源網域 | 低 (可偽造、隱私模式無送出) | 全瀏覽器 | 輔助防禦、非關鍵操作 |
| CSRF Token | 表單隱藏欄位 + Session 比對 | 高 (業界標準) | 全瀏覽器 | 所有狀態改變操作 (登入、轉帳、刪除) |
| SameSite Cookie | 瀏覽器原生阻擋跨站送 Cookie | 中高 (現代瀏覽器預設 Lax) | Chrome 76+、Firefox 69+、Edge 79+ | 深層防禦、配合 Token 使用 |
<?php
function checkReferer(): bool {
// PHP 4.1+ 使用 $_SERVER,舊版用 $HTTP_SERVER_VARS
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if (empty($referer)) {
// 隱私模式、安全軟體、直接輸入網址皆可能無 Referer
// 視風險決定是否阻擋:關鍵操作建議阻擋,一般頁面可放行
return false;
}
$host = parse_url($referer, PHP_URL_HOST) ?? '';
$self = $_SERVER['HTTP_HOST'] ?? '';
// 允許子網域 (可選)
return $host === $self ||
(str_ends_with($host, '.' . $self) && $host !== $self);
}
// 使用
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!checkReferer()) {
http_response_code(403);
die('請求來源驗證失敗 (Referer 異常)');
}
// 處理表單...
}
?>
$_SESSION['csrf_token'] = bin2hex(random_bytes(32))<input type="hidden" name="csrf_token" value="...">hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])<?php
trait CsrfProtection {
private const TOKEN_NAME = 'csrf_token';
private const TOKEN_LENGTH = 32;
// 在表單頁面呼叫:產生並輸出隱藏欄位
public function csrfField(): string {
if (empty($_SESSION[self::TOKEN_NAME])) {
$this->regenerateToken();
}
return sprintf(
'<input type="hidden" name="%s" value="%s">',
self::TOKEN_NAME,
htmlspecialchars($_SESSION[self::TOKEN_NAME], ENT_QUOTES, 'UTF-8')
);
}
// 在處理表單的 Action 呼叫:驗證
public function verifyCsrf(): bool {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; // 僅檢查 POST
$token = $_POST[self::TOKEN_NAME] ?? '';
$sessionToken = $_SESSION[self::TOKEN_NAME] ?? '';
// hash_equals 防止時序攻擊
if (!hash_equals($sessionToken, $token)) {
return false;
}
// 可選:一次性 Token,驗證後重新產生
// $this->regenerateToken();
return true;
}
private function regenerateToken(): void {
$_SESSION[self::TOKEN_NAME] = bin2hex(random_bytes(self::TOKEN_LENGTH));
}
}
// 使用範例
class UserController {
use CsrfProtection;
public function registerForm(): void {
echo '<form method="post" action="/register">';
echo $this->csrfField(); // 自動輸出隱藏欄位
echo '<input name="email" type="email" required>';
echo '<input name="password" type="password" required>';
echo '<button>註冊</button></form>';
}
public function registerAction(): void {
if (!$this->verifyCsrf()) {
http_response_code(403);
die('CSRF Token 驗證失敗,請重新整理頁面再試');
}
// 處理註冊邏輯...
}
}
?>
<?php
// PHP 7.3+ 支援 SameSite 選項
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '', // 當前網域
'secure' => true, // 僅 HTTPS
'httponly' => true, // 阻擋 JS 存取
'samesite' => 'Lax' // 關鍵:Lax 允許導航連結攜帶 Cookie,Strict 完全阻擋
]);
session_start();
// 設定其他 Cookie 同樣加上 SameSite
setcookie('pref', 'dark_mode', [
'expires' => time() + 86400*30,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
?>
| 模式 | 跨站請求攜帶 Cookie | 適用場景 |
|---|---|---|
None | 全部攜帶 (需 Secure) | 第三方嵌入、跨域 API (需配合 CSRF Token) |
Lax (現代預設) | 安全導航 (連結、GET 表單) 攜帶;POST、iframe、AJAX、圖片不攜帶 | 大多數應用 Session Cookie 推薦 |
Strict | 完全不攜帶 (含導航連結) | 高安全需求 (銀行、後台),但使用者體驗差 (點連結需重新登入) |
<?php
// 1. 全域中介層 (所有 POST 請求)
function csrfMiddleware(callable $next): void {
// 略過 API 路由 (使用 Bearer Token 驗證)
if (str_starts_with($_SERVER['REQUEST_URI'], '/api/')) {
$next();
return;
}
// 非 GET/HEAD/OPTIONS 需驗證
if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD', 'OPTIONS'])) {
// 策略 A:SameSite Lax 為第一道防線 (現代瀏覽器自動生效)
// 策略 B:CSRF Token 為核心防線
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; // 支援 AJAX Header
$sessionToken = $_SESSION['csrf_token'] ?? '';
if (!hash_equals($sessionToken, $token)) {
http_response_code(419); // Laravel 慣用 419 CSRF Token Mismatch
header('Content-Type: application/json');
die(json_encode(['error' => 'CSRF token mismatch', 'code' => 'CSRF_INVALID']));
}
}
// 策略 C:Referer 作為輔助 (雙重保險)
if (!checkReferer()) {
error_log("CSRF 疑似攻擊: Referer 異常 IP=" . getClientIp());
// 不直接阻擋,避免誤傷隱私模式使用者,但記錄日誌供稽核
}
$next();
}
// 2. AJAX 請求自動附加 Token
?>
== 比對 Token:必須用 hash_equals() 防止時序攻擊。$_SESSION['csrf_tokens'][],驗證時檢查是否存在並移除該 Token。CSRF Token + SameSite Lax Cookie 是現代 PHP 應用的標準防禦組合。Token 解決核心驗證,SameSite 提供瀏覽器層面原生阻擋,Referer 檢查作為輔助稽核。實作時務必使用 hash_equals()、random_bytes()、session_set_cookie_params() 等安全函式,並支援 AJAX Header 傳遞 Token,構建完整的深層防禦體系。