SQL 注入仍是 Web 應用首要風險。根本對策:預處理語句 + 參數化查詢。PHP 提供兩大主流介面:PDO(跨資料庫抽象層)與 MySQLi(MySQL 專用)。本文對比兩者預處理寫法、核心設定差異、常見誤區與生產環境部署清單。
1. 應用發送 SQL 模板(含佔位符 ? 或 :name)給資料庫伺服器 2. 資料庫解析、編譯、最佳化 → 生成執行計畫(此時無使用者資料) 3. 應用發送參數值 → 資料庫直接套用至執行計畫參數槽 4. 參數值永不經過 SQL 解析器,純粹作為資料處理 → 即使輸入 '; DROP TABLE users; -- 也只會被視為字串字面值查詢
<?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]);
?>
<?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 | MySQLi |
|---|---|---|
| 資料庫支援 | 12+ 驅動 | 僅 MySQL |
| 預處理模式 | 預設模擬,需關閉 | 永遠真實預處理 |
| 參數綁定 | execute([...]) 陣列 | bind_param('types', ...) 逐個 |
| 結果集取得 | 直接迭代 / fetchAll() | get_result() 需 mysqlnd |
| 命名佔位符 | 支援 :name | 僅 ? |
| 交易控制 | beginTransaction() | begin_transaction() |
| 效能 | 微弱抽象開銷 | 原生略快 |
true(PHP 端模擬替換),必須設 false 強制 MySQL 原生預處理。charset 無效,需 SET NAMES utf8mb4。$mysqli->set_charset('utf8mb4') 必須在 prepare() 前呼叫,否則預處理可能使用錯誤編碼導致邊界注入。mysqlnd,get_result() 不可用,需用 bind_result() 逐欄綁定。\.query\(.*\$、mysql_query\(、->query\(.*\.query()、execute()、fetchAll() 方法,內部強制預處理,業務代碼不可直接用 PDO/MySQLi。implode(',', array_fill(0, count($ids), '?'))。| 誤區 | 真相 |
|---|---|
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 字串。