SQL 注入仍是 Web 應用頭號安全風險。本文系統梳理 PHP 防禦 SQL 注入的完整體系:從根本上的參數化查詢,到輔助的輸入過濾、正則檢測、WAF 規則、ORM 安全用法,構建多層防護體系。
這是唯一根本解決方案。將數據與代碼分離,數據庫引擎在解析 SQL 結構時不會將參數視為代碼執行。
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // 關鍵:使用真實預處理
// 佔位符綁定
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND status = ?');
$stmt->execute([$email, 1]);
$user = $stmt->fetch();
// 命名佔位符
$stmt = $pdo->prepare('INSERT INTO logs (user_id, action, ip) VALUES (:uid, :action, :ip)');
$stmt->execute([
'uid' => $userId,
'action' => $action,
'ip' => $clientIp,
]);
?>
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $email); // s=string, i=integer, d=double, b=blob
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
?>
<?php
// 永遠不要這樣寫!
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
$sql = "UPDATE users SET score = $score WHERE id = $id";
?>
參數化查詢解決 99% 注入,但輸入驗證仍是纏深防禦必需:業務邏輯約束、XSS 預防、數據質量保證。
<?php
function validateOrderBy(string $input, array $allowed): string {
$field = strtolower(trim($input));
return in_array($field, $allowed) ? $field : $allowed[0];
}
$orderBy = validateOrderBy($_GET['sort'] ?? '', ['id', 'name', 'created_at', 'score']);
$sql = "SELECT * FROM users ORDER BY $orderBy"; // 安全:值來自白名單
?>
<?php
$id = (int)($_GET['id'] ?? 0); // 強制整數
$limit = max(1, min(100, (int)($_GET['limit'] ?? 20))); // 範圍限制
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL); // 格式驗證
?>
警告:正則無法完美攔截所有注入(編碼繞過、語法變體),不可作為唯一防禦。適用於 WAF 規則、異常日誌告警、蜜罐檢測。
<?php
class SqlInjectionDetector
{
private const PATTERNS = [
// 聯合查詢注入
'/\bUNION\s+ALL\s+SELECT\b/i',
'/\bUNION\s+SELECT\b/i',
// 子查詢注入
'/\bSELECT\s+.*\bFROM\s+.*\bWHERE\b/i',
// 條件永真
'/\bOR\s+[\d\'\"]\s*=\s*[\d\'\"]/i',
'/\bAND\s+[\d\'\"]\s*=\s*[\d\'\"]/i',
// 註釋截斷
'/--\s*$/',
'/\/\*.*\*\//s',
// 危險函數
'/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i',
'/\b(BENCHMARK|SLEEP)\s*\(/i',
// 信息收集
'/\b(INFORMATION_SCHEMA|SYSOBJECTS|SYSCOLUMNS)\b/i',
// 盲注特徵
'/\bIF\s*\(/i',
'/\bCASE\s+WHEN\b/i',
// 編碼繞過
'/(0x[0-9a-f]+|CHAR\(\d+\))/i',
];
public static function detect(string $input): array
{
$matches = [];
foreach (self::PATTERNS as $pattern) {
if (preg_match($pattern, $input)) {
$matches[] = $pattern;
}
}
return $matches;
}
public static function logIfSuspicious(string $input, string $context = ''): void
{
$matches = self::detect($input);
if ($matches) {
error_log("[SQLi Alert] Context: $context | Input: $input | Patterns: " . implode(', ', $matches));
}
}
}
// 使用:中間件中對 $_GET/$_POST/$_COOKIE 遍歷檢測
foreach (array_merge($_GET, $_POST, $_COOKIE) as $key => $val) {
if (is_string($val)) {
SqlInjectionDetector::logIfSuspicious($val, $key);
}
}
?>
# ModSecurity CRS 3.x 關鍵 SQLi 規則 ID
# 942100 - SQL Injection Attack Detected via libinjection
# 942110 - SQL Injection Attack: Common Injection Testing Detected
# 942120 - SQL Injection Attack: Union Query
# 942130 - SQL Injection Attack: Select From
# 942140 - SQL Injection Attack: Insert Into
# 942150 - SQL Injection Attack: Update Set
# 942160 - SQL Injection Attack: Delete From
# 942170 - SQL Injection Attack: Drop Table
# 942180 - SQL Injection Attack: Create Table
# 942190 - SQL Injection Attack: Alter Table
# Nginx + ModSecurity 配置片段
SecRuleEngine On
Include /etc/modsecurity/crs-setup.conf
Include /etc/modsecurity/rules/*.conf
# 調整敏感度(預設 paranoia level 1)
SecAction "id:900000,phase:1,pass,nolog,setvar:tx.paranoia_level=2"
<?php
// ✅ 安全:參數綁定
User::where('email', $email)->first();
User::whereRaw('created_at > ?', [$date])->get();
// ❌ 危險:Raw 未綁定
User::whereRaw("email = '$email'")->first(); // 注入風險
// ✅ 安全:selectRaw 綁定
DB::table('users')->selectRaw('COUNT(*) as cnt WHERE status = ?', [1])->first();
?>
<?php
// ✅ 安全
Db::name('users')->where('email', $email)->find();
Db::name('users')->where('id', 'in', $ids)->select(); // 陣列自動參數化
// ❌ 危險
Db::query("SELECT * FROM users WHERE email = '$email'");
?>
<?php
// 配置兩個數據庫連接
$pdoRead = new PDO('mysql:host=read-replica;dbname=test', 'readonly_user', 'pass');
$pdoWrite = new PDO('mysql:host=master;dbname=test', 'readwrite_user', 'pass');
// 查詢操作用只讀連接(即使注入也無法寫入/刪除)
$stmt = $pdoRead->prepare('SELECT * FROM users WHERE id = ?');
?>
-- MySQL 存儲過程
CREATE PROCEDURE GetUserByEmail(IN p_email VARCHAR(255))
BEGIN
SELECT * FROM users WHERE email = p_email AND status = 1;
END;
// PHP 調用
$stmt = $pdo->prepare('CALL GetUserByEmail(?)');
$stmt->execute([$email]);
?>
GRANT SELECT (id, name, email) ON app.users TO 'api_user'@'%';
GRANT INSERT (user_id, action) ON app.logs TO 'api_user'@'%';
-- 拒絕 DROP、ALTER、TRUNCATE、GRANT 等管理權限
?>
general_log 或 audit_log 記錄所有執行 SQLhoneypot,有值即為機器人/掃描器| 層級 | 措施 | 防禦力度 | 維護成本 |
|---|---|---|---|
| 1. 代碼層(核心) | 全量參數化查詢、PDO::ATTR_EMULATE_PREPARES=false | ★★★★★ | 低(編碼規範) |
| 2. 輸入層 | 白名單驗證、型別強制、長度限制、格式校驗 | ★★★★☆ | 低 |
| 3. ORM/框架層 | 使用安全 API、禁用 Raw SQL、代碼審計規則 | ★★★★☆ | 低 |
| 4. 數據庫層 | 只讀/讀寫分離用戶、最小權限、存儲過程 | ★★★☆☆ | 中 |
| 5. 網絡層 | WAF/ModSecurity CRS、IP 封禁、請求頻率限制 | ★★★☆☆ | 中 |
| 6. 監控層 | 注入特徵日誌告警、蜜罐、審計日誌分析 | ★★★☆☆ | 中 |
金科玉律:參數化查詢是唯一銀彈,其他都是輔助彈藥。沒有參數化查詢,任何過濾都不可靠;有了參數化查詢,其他層級讓防禦更穩健。