首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 表單來源驗證:防止跨站請求偽造 (CSRF) 的實務做法

     編輯:PHP 相關知識

確認表單提交來源為本站,是防禦 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 使用

方案一:HTTP_REFERER 檢查 (簡易但不完美)

<?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 異常)');
    }
    // 處理表單...
}
?>

方案二:CSRF Token (業界標準、高安全性)

核心流程

  1. Session 中產生隨機 Token:$_SESSION['csrf_token'] = bin2hex(random_bytes(32))
  2. 表單隱藏欄位帶入:<input type="hidden" name="csrf_token" value="...">
  3. 提交時比對:hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
  4. 驗證通過後可選擇重新產生 (一次性) 或保留 (多次提交)

完整實作:可重用 Trait

<?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 驗證失敗,請重新整理頁面再試');
        }
        // 處理註冊邏輯...
    }
}
?>

方案三:SameSite Cookie (現代瀏覽器原生防護)

<?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'
]);
?>

SameSite 三種模式差異

模式跨站請求攜帶 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 存放在 Cookie 而非 Session:Cookie 可被子網域竊取,Session 存伺服器端較安全。
  • GET 請求也驗證 Token:GET 應冪等無副作用,無需 CSRF 防護;若 GET 有副作用,先修正 REST 設計。
  • 使用 == 比對 Token:必須用 hash_equals() 防止時序攻擊。
  • 單一 Token 終身不換:登入、權限變更、敏感操作後應重新產生。
  • 忽略多標籤頁場景:若 Token 一次性,使用者開多個標籤頁會失效。建議:Token 存陣列 $_SESSION['csrf_tokens'][],驗證時檢查是否存在並移除該 Token。

總結

CSRF Token + SameSite Lax Cookie 是現代 PHP 應用的標準防禦組合。Token 解決核心驗證,SameSite 提供瀏覽器層面原生阻擋,Referer 檢查作為輔助稽核。實作時務必使用 hash_equals()、random_bytes()、session_set_cookie_params() 等安全函式,並支援 AJAX Header 傳遞 Token,構建完整的深層防禦體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved