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

PHP 安全開發核心準則:輸入驗證、SQL注入防禦、XSS防護、CSRF防範完整指南

     編輯:PHP 相關知識

前言:安全是系統設計的基因,而非事後補丁

PHP 作為最流行的伺服端語言,其安全問題往往源於開發者對「不信任輸入」原則的忽視。本文整理出 PHP 安全開發的六大核心習慣,涵蓋輸入驗證、資料庫防護、配置管理、來源驗證、會話保護、XSS 防禦,每項均提供錯誤與正確代碼對比,助你建立安全第一的開發思維。

習慣一:嚴格的輸入驗證 —— 白名單機制

核心原則:定義「允許什麼」,而非「禁止什麼」。黑名單永遠跟不上攻擊變體。

❌ 錯誤示範:黑名單過濾 / 僅前端驗證

<?php
// 錯誤 1: 嘗試過濾「壞字符」 - 永遠不完整
$username = str_replace(['<', '>', '"', "'", '--', ';'], '', $_POST['username']);

// 錯誤 2: 只信任 JavaScript 前端驗證
// 
// 攻擊者可繞過前端、直接發送 POST 請求、或禁用 JS

✅ 正確示範:白名單驗證 + 伺服端強制執行

<?php
/**
 * 通用輸入驗證函數
 * @param mixed $input 原始輸入
 * @param array $rules 驗證規則 ['type' => 'int|email|alpha|alphanum|regex', ...]
 * @return mixed|false 驗證通過返回過濾後值,失敗返回 false
 */
function validateInput($input, array $rules) {
    // 1. 基礎清理
    if (is_string($input)) {
        $input = trim($input);
        // 移除控制字符 (保留換行/製表視業務需求)
        $input = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $input);
    }

    // 2. 型別/格式驗證
    foreach ($rules as $field => $rule) {
        $value = is_array($input) ? ($input[$field] ?? null) : $input;

        switch ($rule['type']) {
            case 'int':
                if (!filter_var($value, FILTER_VALIDATE_INT, $rule['options'] ?? [])) return false;
                break;
            case 'email':
                if (!filter_var($value, FILTER_VALIDATE_EMAIL)) return false;
                break;
            case 'alpha': // 僅字母
                if (!preg_match('/^[A-Za-z]+$/', $value)) return false;
                break;
            case 'alphanum': // 字母數字
                if (!preg_match('/^[A-Za-z0-9]+$/', $value)) return false;
                break;
            case 'regex':
                if (!preg_match($rule['pattern'], $value)) return false;
                break;
            case 'in_array': // 白名單枚舉
                if (!in_array($value, $rule['values'], true)) return false;
                break;
            case 'string':
                if (!is_string($value)) return false;
                if (isset($rule['min']) && mb_strlen($value) < $rule['min']) return false;
                if (isset($rule['max']) && mb_strlen($value) > $rule['max']) return false;
                break;
        }
    }
    return $input;
}

// --- 使用範例 ---
// 註冊表單驗證
$rules = [
    'username' => ['type' => 'alphanum', 'min' => 3, 'max' => 20],
    'email'    => ['type' => 'email'],
    'age'      => ['type' => 'int', 'options' => ['min_range' => 18, 'max_range' => 120]],
    'role'     => ['type' => 'in_array', 'values' => ['user', 'editor', 'admin']],
    'bio'      => ['type' => 'string', 'max' => 500],
];

$cleanData = validateInput($_POST, $rules);
if ($cleanData === false) {
    http_response_code(400);
    exit('輸入資料格式錯誤');
}
// $cleanData 現在安全可用
?>

習慣二:資料庫安全 —— 參數化查詢 (Prepared Statements)

鐵律:永遠不要將使用者輸入直接拼接進 SQL 字串。mysql_real_escape_string 已棄用 (PHP 7 移除),唯一正解是 PDO / MySQLi Prepared Statements。

❌ 錯誤示範:字串拼接 / 過濾函數

<?php
// 極度危險: 直接拼接
$sql = "SELECT * FROM users WHERE username = '" . $_POST['user'] . "' AND password = '" . $_POST['pass'] . "'";

// 過時且不完全: mysql_real_escape_string (僅對字串有效,數值型注入無效)
// 且 PHP 7+ 已移除 mysql_* 全部函數

✅ 正確示範:PDO Prepared Statements (推薦)

<?php
// 資料庫連線 (單例模式)
class DB {
    private static $pdo = null;
    public static function get(): PDO {
        if (self::$pdo === null) {
            self::$pdo = new PDO(
                'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
                'db_user', 'strong_password',
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false, // 關鍵: 使用真實預處理
                ]
            );
        }
        return self::$pdo;
    }
}

// 安全查詢範例
try {
    $pdo = DB::get();

    // 1. 查詢
    $stmt = $pdo->prepare('SELECT id, username, email FROM users WHERE username = ? AND status = ?');
    $stmt->execute([$cleanData['username'], 1]);
    $user = $stmt->fetch();

    // 2. 插入
    $stmt = $pdo->prepare('INSERT INTO users (username, email, password_hash, created_at) VALUES (?, ?, ?, NOW())');
    $stmt->execute([
        $cleanData['username'],
        $cleanData['email'],
        password_hash($cleanData['password'], PASSWORD_ARGON2ID) // 密碼雜湊
    ]);

    // 3. 更新 (具名參數更易讀)
    $stmt = $pdo->prepare('UPDATE users SET last_login = NOW(), login_ip = :ip WHERE id = :id');
    $stmt->execute([':ip' => $_SERVER['REMOTE_ADDR'], ':id' => $user['id']]);

} catch (PDOException $e) {
    // 生產環境: 記錄錯誤日誌、回傳通用錯誤訊息
    error_log('DB Error: ' . $e->getMessage());
    http_response_code(500);
    exit('系統繁忙,請稍後再試');
}
?>

習慣三:不依賴不安全的 PHP 配置

配置項建議值說明
magic_quotes_gpcOffPHP 5.4 已移除。若開啟會重複轉義導致資料損壞。請自行處理轉義。
register_globalsOffPHP 5.4 已移除。自動將 GET/POST/COOKIE 註冊為全域變數,極度危險。
display_errorsOff (生產環境)錯誤訊息洩漏路徑、SQL、變數值。開發環境可開啟。
log_errorsOn錯誤寫入日誌檔案,而非輸出到瀏覽器。
error_reportingE_ALL & ~E_DEPRECATED & ~E_STRICT開發環境建議 E_ALL;生產環境記錄但不顯示。
expose_phpOff隱藏 HTTP Header 中的 X-Powered-By: PHP/版本資訊。
session.cookie_httponly1防止 JavaScript 存取 Session Cookie (緩解 XSS 竊取)。
session.cookie_secure1 (HTTPS 環境)僅透過 HTTPS 傳輸 Cookie。
session.use_strict_mode1拒絕未初始化的 Session ID,防止 Session Fixation。

習慣四:驗證資料來源 —— CSRF 防護 (Token 模式)

誤區:依賴 $_SERVER['HTTP_REFERER'] —— 易偽造、隱私模式下不送出、代理伺服器可能移除。

✅ 正確實作:雙重 Token 驗證 (表單 + AJAX)

<?php
// 1. 生成 CSRF Token (登入後、表單渲染前)
function generateCsrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// 2. 驗證 CSRF Token (表單提交、狀態變更 API)
function verifyCsrfToken(string $token): bool {
    if (empty($_SESSION['csrf_token']) || empty($token)) return false;
    // 使用 hash_equals 防止時序攻擊
    $valid = hash_equals($_SESSION['csrf_token'], $token);
    // 可選: 一次性 Token 驗證後銷毀 (高安全場景)
    // if ($valid) unset($_SESSION['csrf_token']);
    return $valid;
}

// --- 表單頁面 ---
<form action="/profile/update" method="post">
    <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCsrfToken()) ?>">
    <!-- 其他欄位 -->
    <button type="submit">更新</button>
</form>


<script>
fetch('/api/user/update', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': '<?= generateCsrfToken() ?>' // 從 meta tag 或 JS 變數獲取
    },
    body: JSON.stringify({name: '新名稱'})
});
</script>

<?php
// --- 處理端 ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
    if (!verifyCsrfToken($token)) {
        http_response_code(403);
        exit('CSRF Token 驗證失敗,請重新整理頁面再試');
    }
    // 執行業務邏輯...
}
?>

習慣五:會話資料保護 —— Cookie 安全屬性 + 敏感資料加密

<?php
// Session 安全配置 (必須在 session_start() 之前)
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', isset($_SERVER['HTTPS'])); // 自動偵測 HTTPS
ini_set('session.cookie_samesite', 'Strict'); // 或 'Lax'
ini_set('session.use_strict_mode', 1);
ini_set('session.sid_length', 48); // 更長的 Session ID
session_start();

// 敏感資料加密儲存於 Session (如用戶 ID、權限)
function setSecureSession(string $key, $value): void {
    $key = 'secure_' . $key;
    // 使用 openssl 加密 (需金鑰管理)
    $encKey = hash('sha256', $_ENV['SESSION_ENC_KEY'] ?? 'fallback_key_change_me', true);
    $iv = random_bytes(16);
    $ciphertext = openssl_serialize($value, 'aes-256-gcm', $encKey, OPENSSL_RAW_DATA, $iv, $tag);
    $_SESSION[$key] = base64_encode($iv . $tag . $ciphertext);
}

function getSecureSession(string $key) {
    $key = 'secure_' . $key;
    if (empty($_SESSION[$key])) return null;
    $data = base64_decode($_SESSION[$key], true);
    $iv = substr($data, 0, 16);
    $tag = substr($data, 16, 16);
    $ciphertext = substr($data, 32);
    $encKey = hash('sha256', $_ENV['SESSION_ENC_KEY'] ?? 'fallback_key_change_me', true);
    return openssl_decrypt($ciphertext, 'aes-256-gcm', $encKey, OPENSSL_RAW_DATA, $iv, $tag);
}

// 使用範例
setSecureSession('user_id', 12345);
setSecureSession('permissions', ['read', 'write', 'admin']);
$userId = getSecureSession('user_id'); // 返回 12345 (int)
?>

習慣六:XSS 防禦 —— 輸出編碼 (Context-Aware Encoding)

核心:根據輸出上下文 (HTML Body、HTML Attribute、JavaScript、CSS、URL) 選擇正確編碼函數。htmlspecialchars() 僅適用於 HTML Body/Attribute。

輸出編碼速查表

輸出上下文編碼函數/方法範例
HTML Body / 內容htmlspecialchars($str, ENT_QUOTES, 'UTF-8')<div><?= h($user->bio) ?></div>
HTML Attribute (單/雙引號)htmlspecialchars($str, ENT_QUOTES, 'UTF-8')<input value="<?= h($user->name) ?>">
JavaScript 字串字面量json_encode($str) (含引號)<script>const name = <?= json_encode($user->name) ?>;</script>
URL 參數值rawurlencode($str)<a href="search?q=<?= rawurlencode($kw) ?>">
CSS 值白名單驗證 / cssesc() (需庫)避免直接輸出使用者輸入到 CSS

統一輸出助手函數 (建議專案全域使用)

<?php
// 專案通用輸出函數
function h(string $str, int $flags = ENT_QUOTES): string {
    return htmlspecialchars($str, $flags, 'UTF-8');
}

function hAttr(string $str): string {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

function js(string $str): string {
    return json_encode($str, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}

function url(string $str): string {
    return rawurlencode($str);
}

// 模板中使用
// 
content) ?>
// // // 搜尋 ?>

總結:安全開發檢查清單 (每次 Code Review 必查)

  1. ☐ 所有使用者輸入經過白名單驗證?
  2. ☐ 所有 SQL 查詢使用 Prepared Statements?
  3. ☐ 密碼使用 password_hash() / PASSWORD_ARGON2ID 儲存?
  4. ☐ 生產環境 display_errors = Off、錯誤記錄到日誌?
  5. ☐ 所有狀態變更操作 (POST/PUT/DELETE) 驗證 CSRF Token?
  6. ☐ Session Cookie 設定 HttpOnly、Secure、SameSite?
  7. ☐ 敏感 Session 資料加密儲存?
  8. ☐ 所有輸出根據上下文正確編碼 (h, hAttr, js, url)?
  9. ☐ 檔案上傳:驗證 MIME/副檔名、隨機重命名、儲存於非執行目錄、Nginx/Apache 禁用執行權限?
  10. ☐ 依賴套件定期掃描漏洞 (composer audit、Snyk、Dependabot)?

安全不是功能,是品質的基線。將這六大習慣內化為肌肉記憶,才能在快速迭代中守住底線。

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