SQL 注入仍是 Web 應用頭號風險。單一防禦易失效,**纵深防禦**才是正道。本文總結 7 大必做防範點,構建多層防線。
// ❌ 危險:$id = $_GET['id']; "SELECT * FROM users WHERE id=$id"
// ✅ 安全:強制轉整型/浮點
$id = intval($_GET['id']); // 整數
$price = floatval($_GET['price']); // 浮點
$sql = "SELECT * FROM users WHERE id=$id";
原理:`intval`/`floatval` 會截取數字部分,非數字字符串變為 0,徹底消除注入可能。
// ❌ 危險:addslashes() 不處理字符集、多字節繞過
// ✅ 安全:mysqli_real_escape_string / PDO::quote (需連接上下文)
$name = $mysqli->real_escape_string($_GET['name']);
$sql = "SELECT * FROM users WHERE name='$name'";
⚠️ **注意**:`mysqli_real_escape_string` 需要**已建立連接**且**字符集一致** (推薦 `SET NAMES utf8mb4`)。
// ✅ 最佳實踐:預處理 + 參數綁定
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND status = ?");
$stmt->execute([$id, $status]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
優勢:SQL 解析與數據傳輸分離,參數永遠作為數據而非代碼執行,**從根本上杜絕注入**。
; php.ini
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
生產環境**絕不**向用戶顯示 SQL 錯誤堆棧 (表結構、字段名、SQL 片段全暴露)。
-- 應用賬號僅授予增刪改查
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'%' IDENTIFIED BY 'StrongPass!';
-- 禁止: GRANT FILE, GRANT ALL, GRANT SUPER, GRANT SHUTDOWN
原理:即使注入成功,攻擊者無法 `LOAD_FILE('/etc/passwd')`、`INTO OUTFILE` 寫 webshell、操作系統表。
[客戶端輸入]
↓ (1. 前端校驗 - 體驗非安全)
[WAF/網關層]
↓ (2. 正則攔截明顯攻擊特徵 - 3. Rewrite 隱藏結構)
[應用層 - PHP]
↓ (4. intval/real_escape_string - 5. PDO Prepared Statements 核心)
[數據庫層 - MySQL]
↓ (6. 最小權限賬號 - 7. 錯誤不回顯 - 審計日志)
[業務邏輯]
| 誤區 | 風險 | 正確做法 |
|---|---|---|
| 用 `addslashes()` 防注入 | 多字節字符集 (GBK) 可繞過 | 用驅動級 `real_escape_string` 或 PDO |
| 信任 `PDO::ATTR_EMULATE_PREPARES = true` (默認) | 模擬預處理,本質仍是拼接,有繞過風險 | 設置 `$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);` 使用原生預處理 |
| 框架 ORM 就安全了 | 原生查詢 / 拼接片段 仍可能注入 | 審計所有 `whereRaw`、`execute`、拼接 SQL 代碼 |
| 只過濾關鍵字 `select union` | 大小寫、編碼、註釋繞過無窮無盡 | 參數化查詢才是銀彈 |
**PDO 預處理 (關閉模擬) 是核心銀彈**,配合**最小權限賬號**、**關閉錯誤回顯**、**WAF 前置過濾**、**日志審計**,構建纵深防禦。安全無絕對,但提高攻擊成本至不可接受,即是勝利。