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

PHP 防 SQL 注入最佳實踐:PDO 與 MySQLi 預處理語句完整對比

     編輯:PHP 綜合知識

SQL 注入仍是 Web 應用首要風險。根本對策:預處理語句 + 參數化查詢。PHP 提供兩大主流介面:PDO(跨資料庫抽象層)與 MySQLi(MySQL 專用)。本文對比兩者預處理寫法、核心設定差異、常見誤區與生產環境部署清單。

核心原理:為何預處理能防注入?

1. 應用發送 SQL 模板(含佔位符 ? 或 :name)給資料庫伺服器
2. 資料庫解析、編譯、最佳化 → 生成執行計畫(此時無使用者資料)
3. 應用發送參數值 → 資料庫直接套用至執行計畫參數槽
4. 參數值永不經過 SQL 解析器,純粹作為資料處理
→ 即使輸入 '; DROP TABLE users; -- 也只會被視為字串字面值查詢

方案一:PDO(推薦新專案)

<?php
// 連線設定:關鍵在 ATTR_EMULATE_PREPARES => false
$pdo = new PDO(
    'mysql:host=127.0.0.1;dbname=myapp;charset=utf8mb4',
    'user', 'pass',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false, // 強制真實預處理
    ]
);

// 查詢
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute([':name' => $name]);
foreach ($stmt as $row) {
    // 處理 $row
}

// 新增
$ins = $pdo->prepare('INSERT INTO log (msg, created_at) VALUES (:msg, NOW())');
$ins->execute([':msg' => $unsafeInput]);
?>

方案二:MySQLi(MySQL 專用、程序/物件雙介面)

<?php
$mysqli = new mysqli('127.0.0.1', 'user', 'pass', 'myapp');
$mysqli->set_charset('utf8mb4'); // 必須在查詢前設定

// 物件風格
$stmt = $mysqli->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 's'=string, 'i'=int, 'd'=double, 'b'=blob
$stmt->execute();
$result = $stmt->get_result(); // 需 mysqlnd 驅動
while ($row = $result->fetch_assoc()) {
    // 處理 $row
}
$stmt->close();

// 程序風格
$stmt = mysqli_prepare($mysqli, 'INSERT INTO log (msg) VALUES (?)');
mysqli_stmt_bind_param($stmt, 's', $unsafeInput);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
?>

PDO vs MySQLi 關鍵差異對比

面向PDOMySQLi
資料庫支援12+ 驅動僅 MySQL
預處理模式預設模擬,需關閉永遠真實預處理
參數綁定execute([...]) 陣列bind_param('types', ...) 逐個
結果集取得直接迭代 / fetchAll()get_result() 需 mysqlnd
命名佔位符支援 :name僅 ?
交易控制beginTransaction()begin_transaction()
效能微弱抽象開銷原生略快

關鍵設定陷阱

PDO:ATTR_EMULATE_PREPARES

  • 預設 true(PHP 端模擬替換),必須設 false 強制 MySQL 原生預處理。
  • 舊版 PHP (< 5.3.6) DSN charset 無效,需 SET NAMES utf8mb4。

MySQLi:字元集設定時機

  • $mysqli->set_charset('utf8mb4') 必須在 prepare() 前呼叫,否則預處理可能使用錯誤編碼導致邊界注入。
  • 若無 mysqlnd,get_result() 不可用,需用 bind_result() 逐欄綁定。

實戰防禦清單

  1. 全專案禁用字串拼接 SQL:Code Review / 靜態分析工具掃描 \.query\(.*\$、mysql_query\(、->query\(.*\.
  2. 統一資料庫抽象層:封裝 query()、execute()、fetchAll() 方法,內部強制預處理,業務代碼不可直接用 PDO/MySQLi。
  3. 識別子白名單:表名、欄位名、ORDER BY、ASC/DESC 皆需白名單驗證。
  4. IN 子句動態佔位符:implode(',', array_fill(0, count($ids), '?'))。
  5. 錯誤處理:開發環境顯示詳情,正式環境記錄日誌回傳通用訊息,避免洩露結構。
  6. 最小權限帳號:Web 連線帳號僅擁有必要 CRUD 權限,無 DROP/ALTER/GRANT。
  7. WAF/資料庫防火牆:第二道防線,攔截異常 SQL 模式。

常見誤區澄清

誤區真相
addslashes() / mysql_real_escape_string() 就能防注入編碼、多位元組、驅動版本差異極易繞過,絕不可靠
PDO 預設就是真實預處理預設模擬,必須關閉 EMULATE_PREPARES
存儲過程就安全若存儲過程內部拼接動態 SQL 同樣會被注入
ORM 就不用管ORM 內部用預處理,但 whereRaw()、orderByRaw() 等原生方法仍需自行參數化
只有輸入要過濾輸出才需過濾(XSS),資料庫層面只需參數化

總結

新專案首選 PDO + ATTR_EMULATE_PREPARES=false,跨資料庫、命名佔位符、異常模型現代化。維護舊 MySQLi 專案時,確保 set_charset 正確、使用 bind_param、mysqlnd 可用。核心鐵律:所有動態資料值走參數化,所有識別子走白名單,絕不拼接 SQL 字串。

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