SQL 注入仍是 OWASP Top 10 長年霸榜的高危漏洞。本文系統梳理注入攻擊分類(單查詢、多查詢、盲注、二次注入)、經典案例 (Invision Power Board)、核心防禦原則(參數化查詢、輸入驗證、最小權限)、PDO 預處理標準化範本與避坑指南。
| 類型 | 原理 | 典型 Payload | 危害 |
|---|---|---|---|
| 經典單查詢注入 | 在 WHERE/SET 子句注入恆真條件 | ' OR '1'='1 | 繞過登入、洩露全表資料 |
| UPDATE/DELETE 注入 | 修改 SET 條件或擴大 WHERE 範圍 | ', price=0 WHERE '1'='1 | 資料竄改、全表刪除 |
| 多查詢注入 (堆疊查詢) | 利用 `;` 終止原查詢,附加惡意語句 | '; GRANT ALL...; -- | 提權、建立後門、資料銷毀 |
| 盲注 | 無直接回顯,透過布林/時間推斷 | ' AND (SELECT SLEEP(5))-- | 隱蔽提取敏感資料 |
| 二次注入 | 惡意資料先入庫,後續查詢未轉義觸發 | 註冊 `admin'--`,登入時更新觸發 | 繞過即時防禦,極難偵測 |
// 脆弱代碼
$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 必須經過參數化或嚴格轉義。
<?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 子句 | 無法參數化動態數量佔位符 | 動態產生 `?,?,?` 佔位符陣列 |
<?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 注入風險降至理論最小值。