SQL 注入是 Web 開發中最經典、危害最大的漏洞。本文以登錄頁為例,演示拼接 SQL 的注入過程,並構建**參數化查詢(核心)、輸入驗證/過濾(輔助)、最小權限/監控(纵深)**三層防禦體系。
脆弱代碼:字串拼接 SQL
<?php
// ❌ 極度危險:直接拼接用戶輸入
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $mysqli->query($sql);
if ($result && $result->num_rows > 0) {
// 登錄成功
$_SESSION['user_id'] = $result->fetch_assoc()['id'];
header('Location: /dashboard');
} else {
die('用戶名或密碼錯誤');
}
?>
經典注入 Payload 測試
| 輸入字段 | Payload | 生成 SQL | 效果 |
| password | ' OR '1'='1 | ... password = '' OR '1'='1' | 繞過密碼檢查,登錄首個用戶 |
| username | admin' -- | ... username = 'admin' --' AND password = ... | 註釋掉密碼檢查,以 admin 登錄 |
| username | ' UNION SELECT 1,2,3,4 -- | ... UNION SELECT ... | 聯表查詢竊取其他表數據 |
| password | '; DROP TABLE users; -- | ...; DROP TABLE users; -- | 刪除表(需多語句支援) |
核心防禦:預處理語句
<?php
// ✅ 正確:參數化查詢,數據與代碼徹底分離
$stmt = $mysqli->prepare("SELECT id, password_hash, role FROM users WHERE username = ?");
$stmt->bind_param("s", $username); // "s"=string
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
// 密碼驗證:使用 password_verify,不在 SQL 中比對明文/雜湊
if (password_verify($password, $row['password_hash'])) {
$_SESSION['user_id'] = $row['id'];
$_SESSION['role'] = $row['role'];
header('Location: /dashboard');
exit;
}
}
die('用戶名或密碼錯誤');
?>
輔助防禦:輸入驗證與過濾
<?php
// 1. 白名單驗證:用戶名只允許字母數字下劃線 3-20 位
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
die('用戶名格式錯誤');
}
// 2. 長度限制:防止超長字串導致緩衝區問題
if (strlen($password) > 72) { // bcrypt 最大 72 字節
die('密碼過長');
}
// 3. 通用過濾函數(僅作為輔助,不能替代預處理)
function clean_input(string $input): string {
$input = trim($input);
$input = stripslashes($input); // 兼容 magic_quotes_gpc 遺留
$input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8'); // 防 XSS,非防 SQL 注入
return $input;
}
// 4. 針對 SQL 的轉義(僅在無法使用預處理的極罕見情況下)
$safe = $mysqli->real_escape_string($input); // 僅用於字串字面量,不可用於標識符/數字
?>
纵深防禦:最小權限與監控
- 數據庫用戶最小權限:Web 應用賬號僅擁有
SELECT, INSERT, UPDATE, DELETE 權限,**無 DROP, ALTER, CREATE, GRANT** 權限。
- 禁用多語句執行:
mysqli::multi_query 僅在遷移腳本使用,業務代碼嚴禁。
- WAF / RASP:部署 ModSecurity + OWASP CRS、OpenRASP、或雲廠商 WAF,攔截明顯注入特徵。
- 審計日誌:開啟 MySQL 通用日誌/審計插件,記錄所有失敗查詢、異常模式。
- 錯誤信息不外洩:生產環境
display_errors=Off,用戶看到通用「系統錯誤」,詳細日誌寫入文件供開發排查。
現代框架中的最佳實踐
| 框架/工具 | 防注入機制 |
| Laravel Eloquent | 查詢構建器自動參數化、whereRaw 需手動綁定 |
| Symfony Doctrine | DQL 參數化、setParameter |
| ThinkPHP | Db::name('user')->where('name', $name)->find() 自動參數化 |
| PDO | $stmt = $pdo->prepare(...); $stmt->execute([$param]) |
| Medoo | 鏈式調用自動參數化 |
總結:防 SQL 注入鐵律
- 永不拼接 SQL 字串—— 所有動態值必須用參數化查詢(? / :name)。
- 永不在 SQL 中處理密碼—— 存儲
password_hash(),驗證用 password_verify()。
- 輸入驗證是業務邏輯,非安全邊界—— 白名單正則、長度、類型檢查。
- 數據庫賬號最小權限—— 無 DROP/ALTER,限制單表/列權限(MySQL 8.0+ 支援列級權限)。
- 錯誤不回顯、日誌留存—— 生產環境關閉顯示錯誤,記錄完整堆疊供事後分析。