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

PHP 安全開發七大習慣:從輸入驗證到 CSRF 防護的完整防禦體系

     編輯:PHP 綜合知識

前言

安全不是功能,是過程。本文系統整理 PHP 應用程式開發的七大核心安全習慣,涵蓋輸入、檔案系統、資料庫、Session、XSS、表單偽造、CSRF 七大防線,並提供可直接落地的程式碼模式。

習慣一:嚴格驗證所有輸入

核心原則:白名單 > 黑名單

<?php
// ❌ 黑名單:嘗試過濾壞東西 (永遠不完整)
function badValidate($input) {
    return str_replace(['<script>', 'DROP TABLE', '--'], '', $input);
}

// ✅ 白名單:只接受好東西
function validateUsername(string $input): ?string {
    // 只允許字母數字底線,3-20 字元
    if (preg_match('/^[a-zA-Z0-9_]{3,20}$/', $input)) {
        return $input;
    }
    return null;
}

function validateEmail(string $input): ?string {
    if (filter_var($input, FILTER_VALIDATE_EMAIL)) {
        return $input;
    }
    return null;
}

function validateInt(string $input, int $min = 0, int $max = PHP_INT_MAX): ?int {
    $val = filter_var($input, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => $max]]);
    return $val !== false ? $val : null;
}

// 使用:控制器層統一驗證
$username = validateUsername($_POST['username'] ?? '');
$email = validateEmail($_POST['email'] ?? '');
$age = validateInt($_POST['age'] ?? '', 18, 120);

if ($username === null || $email === null || $age === null) {
    throw new InvalidArgumentException('輸入驗證失敗');
}

關鍵驗證函數

  • filter_var() 內建過濾器 (EMAIL, URL, IP, INT, FLOAT, BOOLEAN, REGEXP)
  • filter_input() 直接從超全域變數取得並驗證
  • ctype_*() 字符類型檢查 (alnum, alpha, digit, xdigit...)
  • mb_check_encoding() 確保編碼合法

習慣二:保護檔案系統

路徑遍歷攻擊防禦

<?php
// ❌ 危險:直接使用用戶輸入路徑
readfile($_GET['file']);

// ✅ 安全:基準目錄 + 實際路徑比對
function safeReadFile(string $baseDir, string $userInput): void {
    $baseDir = realpath($baseDir);
    $requestedPath = realpath($baseDir . DIRECTORY_SEPARATOR . $userInput);
    
    // 確保請求路徑在基準目錄內
    if ($requestedPath === false || strpos($requestedPath, $baseDir) !== 0) {
        throw new RuntimeException('存取被拒絕');
    }
    
    // 可選:限制副檔名
    $allowedExt = ['pdf', 'jpg', 'png', 'txt'];
    $ext = strtolower(pathinfo($requestedPath, PATHINFO_EXTENSION));
    if (!in_array($ext, $allowedExt)) {
        throw new RuntimeException('檔案類型不允許');
    }
    
    // 安全輸出
    header('Content-Type: ' . mime_content_type($requestedPath));
    header('Content-Disposition: inline; filename="' . basename($requestedPath) . '"');
    readfile($requestedPath);
}

上傳檔案安全清單

  • 副檔名白名單 + MIME 型別驗證 (finfo)
  • 檔名隨機化儲存,原檔名存資料庫
  • 上傳目錄禁用腳本執行 (Nginx/Apache 配置)
  • 檔案大小限制 (PHP + Web Server 雙重)
  • 掃描惡意內容 (ClamAV、VirusTotal API)

習慣三:保護資料庫 (防 SQL 注入)

參數化查詢:唯一正解

<?php
// ❌ 危險:字串拼接
$sql = "SELECT * FROM users WHERE name = '$name'";

// ✅ PDO 參數綁定
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name');
$stmt->execute([':name' => $name]);
$user = $stmt->fetch();

// ✅ 多條件動態查詢建構器
function buildWhere(array $filters, array $allowedFields): array {
    $clauses = [];
    $params = [];
    foreach ($filters as $field => $value) {
        if (!in_array($field, $allowedFields)) continue;
        $clauses[] = "$field = :$field";
        $params[":$field"] = $value;
    }
    return [implode(' AND ', $clauses), $params];
}

[$where, $params] = buildWhere($_GET, ['status', 'category', 'author']);
$sql = "SELECT * FROM posts" . ($where ? " WHERE $where" : "");
$stmt = $pdo->prepare($sql);
$stmt->execute($params);

額外防禦層

  • 資料庫用戶最小權限:Web 應用僅需 SELECT/INSERT/UPDATE,無 DROP/ALTER
  • 錯誤訊息不顯示給用戶:生產環境關閉 display_errors,記錄 log
  • 使用 ORM/Query Builder (Eloquent, Doctrine, Cycle) 降低手寫 SQL 風險

習慣四:保護 Session 資料

Session 安全配置

<?php
// 啟動前配置 (必須在 session_start() 前)
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);        // 僅 HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', 1);
ini_set('session.gc_maxlifetime', 1800);    // 30 分鐘
ini_set('session.sid_length', 48);
ini_set('session.sid_bits_per_character', 6);

// 自訂儲存處理器 (Redis 範例)
class RedisSessionHandler implements SessionHandlerInterface {
    private $redis, $prefix = 'sess:', $ttl = 1800;
    public function __construct(Redis $redis) { $this->redis = $redis; }
    public function open($path, $name) { return true; }
    public function close() { return true; }
    public function read($id) { return $this->redis->get($this->prefix . $id) ?: ''; }
    public function write($id, $data) { return $this->redis->setex($this->prefix . $id, $this->ttl, $data); }
    public function destroy($id) { return $this->redis->del($this->prefix . $id); }
    public function gc($maxlifetime) { return true; } // Redis 自動過期
}
session_set_save_handler(new RedisSessionHandler($redis), true);
session_start();

敏感資料加密儲存

// 寫入前加密
$_SESSION['sensitive'] = sodium_crypto_secretbox(
    json_encode($data),
    random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES),
    $key // 從環境變數/金鑰管理系統取得
);

// 讀取時解密
$data = json_decode(sodium_crypto_secretbox_open(
    $_SESSION['sensitive'],
    $nonce,
    $key
), true);

習慣五:防禦 XSS (跨站腳本攻擊)

輸出編碼:語境決定編碼方式

輸出語境編碼函數範例
HTML 內容htmlspecialchars($str, ENT_QUOTES, 'UTF-8')<div>{$safe}</div>
HTML 屬性htmlspecialchars($str, ENT_QUOTES, 'UTF-8')<input value="{$safe}">
JavaScript 字串json_encode($str) (含引號)const msg = {$safe};
URL 參數rawurlencode($str)<a href="?q={$safe}">
CSS 值preg_replace('/[^a-zA-Z0-9_-]/', '', $str)color: {$safe};

現代模板引擎自動逃逸

{-- Blade (Laravel) --}
<div>{{ $userInput }}</div> {{-- 自動 htmlspecialchars --}}
<div>{!! $trustedHtml !!}</div> {{-- 故意不逃逸,極謹慎 --}}

{# Twig (Symfony) #}
<div>{{ userInput }}{# 自動逃逸 #}
<div>{{ trustedHtml|raw }}{# 故意不逃逸 #}

內容安全政策 (CSP) - 深度防禦

header("Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';");

習慣六:驗證表單來源 (防表單偽造)

CSRF Token 模式

<?php
// 產生 Token (Session 儲存)
function generateCsrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// 驗證 Token
function verifyCsrfToken(string $token): bool {
    if (empty($_SESSION['csrf_token'])) return false;
    return hash_equals($_SESSION['csrf_token'], $token);
}

// 表單中嵌入
echo '<input type="hidden" name="csrf_token" value="' . generateCsrfToken() . '">';

// 處理端驗證
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        exit('CSRF Token 無效');
    }
    // 處理業務邏輯...
}

SameSite Cookie 現代防禦

SameSite: Lax (預設) 可阻擋大部分跨站 POST 請求攜帶 Cookie。關鍵操作 (刪除、轉帳) 仍需 CSRF Token 雙重保障。

習慣七:防禦 CSRF (跨站請求偽造)

攻擊原理

受害者登入 A 網站 → 瀏覽惡意 B 網站 → B 網站自動提交表單到 A 網站 → 瀏覽器攜帶 A 的 Cookie → A 執行操作。

完整防禦策略

  1. CSRF Token:所有狀態改變操作 (POST/PUT/DELETE) 必須驗證
  2. SameSite Cookie:Lax 或 Strict
  3. 雙重 Cookie 模式:JS 可讀取的 Cookie + HttpOnly Cookie 比對 (SPA 適用)
  4. 自定義標頭:API 要求 X-Requested-With: XMLHttpRequest (簡單但可繞過)
  5. 驗證 Referer/Origin:輔助檢查,不可單獨依賴

框架整合範例

// Laravel: 所有非 GET/HEAD 路由自動驗證 (VerifyCsrfToken 中介軟體)
// Blade: @csrf 指令自動生成隱藏欄位

// Symfony: CsrfTokenManager + 表單類型自動整合
$form = $this->createFormBuilder()
    ->add('task', TextType::class)
    ->getForm(); // 自動加入 _token 欄位

總結:安全開發檢查清單

習慣核心動作工具/函數
1. 輸入驗證白名單、型別檢查、長度限制filter_var, preg_match, ctype_*
2. 檔案系統路徑比對、副檔名白名單、禁用執行realpath, finfo, Web Server 配置
3. 資料庫參數化查詢、最小權限、錯誤隱藏PDO prepare, ORM
4. Session安全 Cookie、Redis 儲存、敏感加密ini_set, SessionHandlerInterface, sodium
5. XSS語境編碼、CSP、模板引擎自動逃逸htmlspecialchars, json_encode, CSP Header
6. 表單偽造CSRF Token、SameSite Cookierandom_bytes, hash_equals
7. CSRFToken 驗證、SameSite、Referer 檢查中介軟體、框架內建

結語

安全是系統工程,非單點修補。將這七大習慣內化為團隊開發規範、代碼審查核心項目、CI/CD 自動化檢查 (SAST/DAST),才能構建真正可信賴的 PHP 應用。

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