`addslashes()`、`mysql_real_escape_string()`、**`magic_quotes_gpc`** 均屬**歷史產物**,存在繞過風險 (多字節編碼、邊界情況)、維護成本高、**已在 PHP 7.0+ 移除**。**唯一標準防禦:預處理語句 + 參數綁定 (Prepared Statements / Parameterized Queries)。**
<?php
$str = "O'Reilly";
$safe = addslashes($str); // O\'Reilly
// 缺陷:不處理 % _ 等 LIKE 通配符;GBK 多字節繞過 (0xbf27 吃掉 \);無法防禦二階注入
<?php
// php.ini: magic_quotes_gpc = On
// 自動對 $_GET/$_POST/$_COOKIE 執行 addslashes()
// 災難:雙重轉義、污染數據源、誤導開發者、性能損耗、多字節繞過
// 檢測兼容代碼 (歷史項目維護)
if (get_magic_quotes_gpc()) {
$_GET = array_map('stripslashes', $_GET);
$_POST = array_map('stripslashes', $_POST);
$_COOKIE = array_map('stripslashes', $_COOKIE);
}
依賴當前連接字符集,**必須在設置字符集後調用**,否則繞過風險極高。PDO/MySQLi 同理需 `mysqli_real_escape_string($link, $str)`。
<?php
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 關鍵:使用真實預處理 (服務端)
]);
// 1. 佔位符綁定 (推薦命名佔位符,可讀性高)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([
':email' => $userInputEmail, // 自動處理引號、特殊字符、編碼
':status' => 1,
]);
$user = $stmt->fetch();
// 2. 批量插入 (事務 + 預處理)
$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO logs (user_id, action, ip) VALUES (?, ?, ?)');
foreach ($logs as $log) {
$stmt->execute([$log['user_id'], $log['action'], $log['ip']]);
}
$pdo->commit();
// 3. LIKE 查詢 (通配符在綁定值中拼接,而非 SQL 中)
$keyword = '%' . $userKeyword . '%'; // PHP 端拼接 %
$stmt = $pdo->prepare('SELECT * FROM articles WHERE title LIKE ?');
$stmt->execute([$keyword]);
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$mysqli->set_charset('utf8mb4');
// 物件風格
$stmt = $mysqli->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->bind_param('s', $email); // 's'=string, 'i'=int, 'd'=double, 'b'=blob
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) { /* ... */ }
$stmt->close();
<?php
// ❌ 錯誤:無法綁定表名/列名
// $stmt = $pdo->prepare("SELECT * FROM ? WHERE ? = ?"); // 報錯
// ✅ 正確:白名單驗證 + 反引號包裹
$allowedTables = ['users', 'orders', 'products'];
$allowedColumns = ['id', 'name', 'email', 'created_at'];
$table = in_array($_GET['table'], $allowedTables) ? $_GET['table'] : 'users';
$col = in_array($_GET['sort'], $allowedColumns) ? $_GET['sort'] : 'id';
$dir = ($_GET['dir'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';
$sql = sprintf('SELECT * FROM `%s` ORDER BY `%s` %s', $table, $col, $dir);
// $sql 現在安全,因為標識符來自白名單
$stmt = $pdo->query($sql);
<?php
$ids = [1, 5, 12, 99]; // 動態數組
$placeholders = implode(',', array_fill(0, count($ids), '?')); // ?,?,?,?
$sql = "SELECT * FROM users WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute($ids);
**原理**:惡意數據首次入庫時合法 (經參數化),二次業務中被拼接進 SQL 執行。
**防禦**:**所有**進入 SQL 的變量**無一例外**使用參數化查詢,包括來自數據庫的讀取值。
避免在存儲過程中拼接動態 SQL。若必須,數據庫層面使用 `PREPARE`/`EXECUTE` 或 `sp_executesql` (SQL Server) 參數化。
| 框架 | ORM/查詢構建器 | 防注入機制 |
|---|---|---|
| Laravel | Eloquent / DB::table() | 所有 `where`/`whereIn`/`update`/`insert` 內部統一 PDO 參數化 |
| Symfony | Doctrine DQL / QueryBuilder | DQL 編譯為參數化 SQL;原生 SQL 需手動 `setParameter` |
| ThinkPHP 6/8 | Db::name() / Model | 查詢構建器自動參數化;`whereRaw` 需謹慎 |
| CodeIgniter 3/4 | Query Builder | `$this->db->where()` 參數化;`query()` 需手動 `$this->db->escape()` |
| Yii 2 | ActiveRecord / Query | 參數化綁定;`where(['col' => $val])` 安全 |
[ ] 所有 SQL 執行路徑使用 prepare()/bindParam()/execute()
[ ] 無字符串拼接 SQL (包括 . 拼接、雙引號內變量、sprintf、vsprintf)
[ ] 動態表名/列名/排序/分頁參數經過白名單驗證
[ ] IN 子句動態生成佔位符
[ ] 數據庫連接字符集 utf8mb4 (防多字節繞過)
[ ] 錯誤信息不回顯給前端 (生產關閉 display_errors、PDO::ERRMODE_EXCEPTION 記錄日誌)
[ ] 最小權限數據庫賬號 (僅 SELECT/INSERT/UPDATE/DELETE,無 DROP/ALTER/GRANT)
[ ] WAF/防火牆部署 (ModSecurity + OWASP CRS) 作為第二道防線
[ ] 定期 SQL 注入掃描 (sqlmap 自動化測試)
[ ] 代碼審計工具集成 (PHPStan + phpstan-security, Psalm, SonarQube)
| 誤區 | 真相 |
|---|---|
| 「我用 `addslashes` 就安全了」 | GBK 編碼下 `0xbf27` 吃掉反斜杠,繞過轉義。參數化才是銀彈。 |
| 「PDO 模擬預處理 (`ATTR_EMULATE_PREPARES=true`) 也安全」 | 客戶端模擬轉義,**不如服務端真實預處理安全**,邊界情況可能繞過。**務必設置 `false`**。 |
| 「ORM 就絕對安全」 | `whereRaw`、`expr`、`orderByRaw`、`havingRaw` 等原生方法**不自動參數化**,需手動綁定。 |
| 「轉義函數能防禦所有注入」 | 無法防禦二階注入、標識符注入、寬字節注入、邊界繞過。 |
| 「WAF 能攔截所有攻擊」 | WAF 基於特徵庫,存在繞過 (編碼、大小寫、註釋、HTTP 分塊)。**代碼層面參數化是根本**。 |
鐵律:**「參數化查詢」是 SQL 注入的唯一標準答案。** 任何手動轉義、拼接、過濾、WAF 依賴均為危險補丁。新項目從第一行代碼開始強制參數化,舊項目重構優先清理拼接 SQL。數據庫賬號最小權限、錯誤不回顯、定期掃描,構建纵深防禦。