資料庫是應用最核心資產。本文透過脆弱代碼實測、攻擊向量分析、參數化查詢原理、多層防禦架構,建立完整資料庫安全防護體系。
<?php
// 脆弱代碼:動態拼接 SQL
$col = $_POST['col']; // 未驗證的欄位名
$account = $_POST['account_number']; // 未驗證的帳號
$sql = "SELECT $col FROM account_data WHERE account_number = $account;";
// 攻擊輸入:account_number = "1 OR 1=1" → 洩露所有帳號
// 攻擊輸入:account_number = "1; DROP TABLE account_data;--" → 刪除資料表
// 攻擊輸入:col = "* FROM users;--" → 橫向越權讀取用戶表
$result = mysql_query($sql); // mysql_* 已移除,僅示意
?>
HTML `
結論:所有來自客戶端的資料(`$_GET`, `$_POST`, `$_COOKIE`, `$_REQUEST`, `$_SERVER`, `$_FILES`)皆不可信。
<?php
// PDO Prepared Statement - 唯一正解
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 關鍵:使用真實預處理
]);
$stmt = $pdo->prepare('SELECT account_number, name, address FROM account_data WHERE account_number = ?');
$stmt->execute([$_POST['account_number']]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 命名參數(可讀性更好)
$stmt = $pdo->prepare('SELECT * FROM account_data WHERE account_number = :acc AND status = :status');
$stmt->execute([':acc' => $account, ':status' => 'active']);
?>
| 階段 | 傳統拼接 | 參數化查詢 |
|---|---|---|
| SQL 解析 | 含用戶資料的完整 SQL 送入解析器 | 只有佔位符 `?` 的 SQL 送入解析器,生成查詢計畫 |
| 參數綁定 | 無(資料已在 SQL 字串中) | 資料作為純值綁定,永遠不會被解析為 SQL 語法 |
| 執行 | 直接執行 | 使用預先編譯計畫 + 參數值執行 |
資料與代碼徹底分離,注入根本不可能發生。
參數化查詢無法用於識別欄位(column)、資料表、排序方向等 SQL 結構元素。
<?php
// 白名單驗證:唯一安全方式
$allowedColumns = ['account_number', 'name', 'address', 'email', 'created_at'];
$col = $_POST['col'] ?? 'account_number';
if (!in_array($col, $allowedColumns, true)) {
throw new InvalidArgumentException('非法欄位名');
}
$stmt = $pdo->prepare("SELECT $col FROM account_data WHERE account_number = ?");
$stmt->execute([$account]);
?>
<?php
function isValidAccountNumber($number): bool {
// 純數字、長度限制
return (bool)preg_match('/^\d{6,20}$/', $number);
}
// 使用
$account = $_POST['account_number'] ?? '';
if (!isValidAccountNumber($account)) {
die('帳號格式錯誤');
}
// 然後才進入參數化查詢
$stmt = $pdo->prepare('SELECT ... WHERE account_number = ?');
$stmt->execute([$account]);
?>
| 做法 | 風險 | 狀態 |
|---|---|---|
| magic_quotes_gpc | 自動跳脫汙染資料、雙重跳脫、誤導安全感 | PHP 5.4 移除 |
| mysql_real_escape_string() | 需連線資源、編碼設定易出錯、僅限 MySQL | PHP 7.0 移除 (mysql_*) |
| addslashes() | 非資料庫專用、多位元組繞過 | 不推薦 |
| 自訂跳脫函數 | 極易有疏漏、維護成本高 | 禁止 |
┌─────────────────────────────────────────────────────────────┐
│ L1: WAF / API Gateway (SQLi 規則集、速率限制、IP 信譽) │
├─────────────────────────────────────────────────────────────┤
│ L2: Controller / FormRequest (白名單驗證、型別轉換、長度) │
├─────────────────────────────────────────────────────────────┤
│ L3: Service / Repository (參數化查詢、白名單欄位、事務控制) │
├─────────────────────────────────────────────────────────────┤
│ L4: Database User (最小權限:僅 SELECT/INSERT/UPDATE,無 DROP)│
├─────────────────────────────────────────────────────────────┤
│ L5: Database Engine (Prepared Statement 快取、查詢審計日誌) │
└─────────────────────────────────────────────────────────────┘
<?php
// 慢查詢 + 異常查詢日誌
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); // 自行處理
function loggedExecute(PDO $pdo, string $sql, array $params = []): PDOStatement {
$start = microtime(true);
$stmt = $pdo->prepare($sql);
$result = $stmt->execute($params);
$duration = microtime(true) - $start;
// 記錄:SQL 模板、參數(脫敏)、耗時、影響行數
if ($duration > 1.0 || !$result) {
error_log(json_encode([
'sql' => $sql,
'params' => array_map(fn($v) => is_string($v) && strlen($v) > 50 ? substr($v,0,50).'...' : $v, $params),
'duration_ms' => round($duration * 1000, 2),
'row_count' => $stmt->rowCount(),
'trace' => array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 0, 3)
], JSON_UNESCAPED_UNICODE));
}
return $stmt;
}
?>
資料庫安全的鐵律:參數化查詢為主、白名單驗證為輔、最小權限為本、審計日誌為證。將這些實作封裝為專案共用的 Repository 基類,確保全團隊一致防護。