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

PHP 防 SQL 注入完整防禦指南:從原理分類到 PDO 預處理最佳實踐

     編輯:PHP 相關知識

SQL 注入仍是 OWASP Top 10 長年霸榜的高危漏洞。本文系統梳理注入攻擊分類(單查詢、多查詢、盲注、二次注入)、經典案例 (Invision Power Board)、核心防禦原則(參數化查詢、輸入驗證、最小權限)、PDO 預處理標準化範本與避坑指南。

SQL 注入攻擊分類與原理

類型原理典型 Payload危害
經典單查詢注入在 WHERE/SET 子句注入恆真條件' OR '1'='1繞過登入、洩露全表資料
UPDATE/DELETE 注入修改 SET 條件或擴大 WHERE 範圍', price=0 WHERE '1'='1資料竄改、全表刪除
多查詢注入 (堆疊查詢)利用 `;` 終止原查詢,附加惡意語句'; GRANT ALL...; --提權、建立後門、資料銷毀
盲注無直接回顯,透過布林/時間推斷' AND (SELECT SLEEP(5))--隱蔽提取敏感資料
二次注入惡意資料先入庫,後續查詢未轉義觸發註冊 `admin'--`,登入時更新觸發繞過即時防禦,極難偵測

經典案例複盤:Invision Power Board (2005)

// 脆弱代碼
$DB->query("SELECT * FROM ibf_members WHERE id=$mid AND password='$pid'");

// $pid 直接來自 Cookie 未處理
$pid = $std->my_getcookie('pass_hash'); // 返回 raw cookie value

// 攻擊 Cookie: pass_hash = ' OR '1'='1
// 產生 SQL: SELECT * FROM ibf_members WHERE id=123 AND password='' OR '1'='1'
// 結果:繞過密碼驗證直接登入
> **修正**:所有外部輸入進入 SQL 必須經過參數化或嚴格轉義。

核心防禦五大支柱

  1. 參數化查詢 (Prepared Statements) - 核心防線
    • 資料與代碼分離,資料庫引擎保證不會被解析為 SQL 指令。
    • PDO / mysqli 皆支援,禁止手工拼接 SQL 字串。
  2. 輸入驗證與白名單
    • 型別檢查:`is_int()`, `filter_var($id, FILTER_VALIDATE_INT)`。
    • 格式驗證:Email、手機、身分證用 Regex 白名單。
    • 長度限制:`strlen($input) <= MAX_LEN`。
    • 枚舉值白名單:`in_array($status, ['active','inactive'])`。
  3. 最小權限資料庫帳號
    • Web 應用布號僅擁有 `SELECT/INSERT/UPDATE/DELETE` 特定表權限。
    • 嚴禁 `GRANT ALL`、`DROP`、`CREATE`、`FILE` 權限。
    • 不同功能模組使用不同 DB 帳號 (讀/寫分離)。
  4. 錯誤資訊隱藏
    • 生產環境 `display_errors = Off`,`log_errors = On`。
    • 自定義錯誤頁面,不洩露 SQL 結構、表名、欄位。
  5. WAF / RASP 運行時防護
    • ModSecurity + OWASP CRS 規則集。
    • OpenRASP / Sqreen 等 RASP 代理掛載。

PDO 預處理標準化範本 (生產級)

<?php
class SafeDatabase {
    private PDO $pdo;

    public function __construct(string $dsn, string $user, string $pass) {
        $this->pdo = new PDO($dsn, $user, $pass, [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false, // 關鍵:使用原生預處理
            PDO::MYSQL_ATTR_SSL_CA       => '/path/to/ca.pem', // 生產必開 SSL
        ]);
    }

    // 安全查詢:回傳單行
    public function fetchOne(string $sql, array $params = []): array|false {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch() ?: false;
    }

    // 安全查詢:回傳多行
    public function fetchAll(string $sql, array $params = []): array {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }

    // 安全寫入:回傳影響行數/最後插入 ID
    public function execute(string $sql, array $params = []): int {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->rowCount();
    }

    public function lastInsertId(string $sequence = ''): string {
        return $this->pdo->lastInsertId($sequence);
    }

    // 事務包裝
    public function transaction(callable $callback): mixed {
        $this->pdo->beginTransaction();
        try {
            $result = $callback($this);
            $this->pdo->commit();
            return $result;
        } catch (Throwable $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }
}

// === 使用範例 ===
$db = new SafeDatabase('mysql:host=127.0.0.1;dbname=app;charset=utf8mb4', 'web_user', 'strong_pass');

// 參數化查詢 (佔位符 ? 或 :name)
$user = $db->fetchOne('SELECT * FROM users WHERE email = ? AND status = ?', [$email, 'active']);

// 批次插入
$ids = $db->transaction(function($db) use ($data) {
    $stmt = $db->pdo->prepare('INSERT INTO logs (user_id, action, ip) VALUES (?, ?, ?)');
    foreach ($data as $row) {
        $stmt->execute([$row['uid'], $row['act'], $row['ip']]);
    }
    return $db->pdo->lastInsertId();
});
?>

歷史誤區與現代替代

過時做法問題現代替代
`mysql_real_escape_string()`需連線、字元集易錯、不支援預處理PDO 參數化查詢
`addslashes()`不處理字元集、多位元組繞過參數化查詢
`magic_quotes_gpc`PHP 5.4 移除、污染全域變數參數化查詢
手工 `implode` 建構 IN 子句無法參數化動態數量佔位符動態產生 `?,?,?` 佔位符陣列

動態 IN 子句參數化技巧

<?php
$ids = [1, 5, 12, 99];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql = "SELECT * FROM products WHERE id IN ($placeholders) AND status = ?";
$params = array_merge($ids, ['active']);
$results = $db->fetchAll($sql, $params);
?>

代碼審計檢查清單

  • [ ] 所有 SQL 是否使用 `prepare()` + `execute()`?
  • [ ] 是否有 `EMULATE_PREPARES => false`?
  • [ ] 動態表名/欄位名是否經過白名單驗證?
  • [ ] 錯誤日誌是否記錄完整 SQL (脫敏後) 供事後分析?
  • [ ] 定期執行 SQLMap / 自動化掃描驗證?

堅持「參數化查詢為唯一核心防線,輸入驗證為輔助,最小權限為保底」三層防禦體系,能將 SQL 注入風險降至理論最小值。

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