SQL 注入 是將惡意 SQL 片段透過應用程式輸入傳遞,竄改原本查詢邏輯,導致資料洩露、竄改、刪除甚至伺服器被控制。本文系統整理攻擊分類、注入點特徵、繞過技術,並提供從程式碼、資料庫、環境、架構四層面的完整防禦體系。
| 類型 | 注入點位置 | 典型 Payload | 危害 |
|---|---|---|---|
| SELECT 注入 | WHERE, ORDER BY, LIMIT | ' OR '1'='1, UNION SELECT... | 繞過驗證、讀取敏感表、導出檔案 |
| UPDATE 注入 | SET 值, WHERE 條件 | ', level='3, ') WHERE username='admin'# | 提權、修改任意用戶資料 |
| INSERT 注入 | VALUES 列表 | ', '3')# | 註冊提權、寫入惡意資料 |
| DELETE 注入 | WHERE 條件 | ' OR '1'='1 | 刪除全表資料 |
| 多語句注入 | 任何支援多語句的驅動 | '; GRANT ALL...;-- | 執行任意 SQL、建立後門帳號 |
query() 只執行第一條)。需呼叫 mysqli_multi_query() 才能執行多條。PDO::MYSQL_ATTR_MULTI_STATEMENTS)。sqlite_query() 若用於 SELECT 則不支援多語句。
// 易受攻擊代碼
$mid = intval($cookie['member_id']); // 強制整數,安全
$pid = $cookie['pass_hash']; // 直接用 Cookie 值,未過濾!
$sql = "SELECT * FROM ibf_members WHERE id=$mid AND password='$pid'";
// 攻擊:修改 Cookie pass_hash = ' OR '1'='1
// 產生: ... WHERE id=123 AND password='' OR '1'='1'
教訓:任何外部輸入 (含 Cookie、Header、Session 讀取值) 進入 SQL 必須視為不信任,採用參數化查詢。
SQL 邏輯先編譯,資料後綁定,資料永不被解析為 SQL 指令。
// PDO (推薦)
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND status = 1");
$stmt->execute([':username' => $inputUsername]);
$user = $stmt->fetch();
// MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $inputUsername);
$stmt->execute();
即使使用跳脫函數,WHERE id = $id (無引號) 仍可被 1 OR 1=1 注入。正確:WHERE id = '$id' (配合跳脫/預處理)。
$id = (int)$_GET['id']; 或 intval()。$price = (float)$_POST['price'];。in_array($sort, ['asc','desc'], true),in_array($col, $allowedColumns, true)。strlen(), preg_match() 白名單正則。if (strpos($input, ';') !== false) die(); (針對極少數無法用預處理的舊代碼)。GRANT SELECT, INSERT, UPDATE, DELETE ON db.* TO 'webuser'@'localhost'。FILE (導出/讀取檔案)、GRANT, SUPER, PROCESS, SHUTDOWN, CREATE USER。; php.ini 生產環境建議
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
disable_functions = exec,system,passthru,shell_exec,proc_open,popen,fsockopen,pcntl_exec,...
; 視專案需求保留必要函數
僅作為臨時緩解,必須規劃重構時程。
function safeQuery($mysqli, $sql, $params, $types) {
$stmt = $mysqli->prepare($sql);
if ($params) $stmt->bind_param($types, ...$params);
$stmt->execute();
return $stmt;
}
// 將舊代碼
// $sql = "SELECT * FROM t WHERE name = '$name' AND age > $age";
// 改為
$stmt = safeQuery($mysqli, "SELECT * FROM t WHERE name = ? AND age > ?", [$name, $age], 'si');
若極少數場景無法用預處理 (如動態 ORDER BY 欄位),必須嚴格白名單驗證:
$allowed = ['id','name','created_at'];
$order = in_array($_GET['order'], $allowed, true) ? $_GET['order'] : 'id';
$sql = "SELECT * FROM t ORDER BY `$order`"; // 反引號包裹識別碼
display_errors = Off,錯誤寫入日誌。SQL 注入防禦的核心唯有一條:**永遠不要將不可信資料拼接進 SQL 字串**。參數化查詢是實現此原則的標準工具。其他措施 (跳脫、驗證、權限、WAF) 皆為輔助纏深度防禦,無法替代參數化查詢。