首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP SQL 注入防護全攻略:從 addslashes 到參數化查詢的現代最佳實踐

     編輯:PHP 綜合知識

核心結論:拒絕手動轉義,擁抱參數化查詢

`addslashes()`、`mysql_real_escape_string()`、**`magic_quotes_gpc`** 均屬**歷史產物**,存在繞過風險 (多字節編碼、邊界情況)、維護成本高、**已在 PHP 7.0+ 移除**。**唯一標準防禦:預處理語句 + 參數綁定 (Prepared Statements / Parameterized Queries)。**

歷史方法淺析 (僅供理解舊代碼,嚴禁新項目使用)

1. `addslashes()` - 簡單字符轉義

<?php
$str = "O'Reilly";
$safe = addslashes($str); // O\'Reilly
// 缺陷:不處理 % _ 等 LIKE 通配符;GBK 多字節繞過 (0xbf27 吃掉 \);無法防禦二階注入

2. `magic_quotes_gpc` - 自動轉義 (PHP 5.4 移除)

<?php
// php.ini: magic_quotes_gpc = On
// 自動對 $_GET/$_POST/$_COOKIE 執行 addslashes()
// 災難:雙重轉義、污染數據源、誤導開發者、性能損耗、多字節繞過
// 檢測兼容代碼 (歷史項目維護)
if (get_magic_quotes_gpc()) {
    $_GET = array_map('stripslashes', $_GET);
    $_POST = array_map('stripslashes', $_POST);
    $_COOKIE = array_map('stripslashes', $_COOKIE);
}

3. `mysql_real_escape_string()` - MySQL 專用 (隨 `mysql_*` 擴展移除)

依賴當前連接字符集,**必須在設置字符集後調用**,否則繞過風險極高。PDO/MySQLi 同理需 `mysqli_real_escape_string($link, $str)`。

現代標準:PDO 預處理語句 (推薦)

基礎用法

<?php
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false, // 關鍵:使用真實預處理 (服務端)
]);

// 1. 佔位符綁定 (推薦命名佔位符,可讀性高)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([
    ':email' => $userInputEmail,   // 自動處理引號、特殊字符、編碼
    ':status' => 1,
]);
$user = $stmt->fetch();

// 2. 批量插入 (事務 + 預處理)
$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO logs (user_id, action, ip) VALUES (?, ?, ?)');
foreach ($logs as $log) {
    $stmt->execute([$log['user_id'], $log['action'], $log['ip']]);
}
$pdo->commit();

// 3. LIKE 查詢 (通配符在綁定值中拼接,而非 SQL 中)
$keyword = '%' . $userKeyword . '%'; // PHP 端拼接 %
$stmt = $pdo->prepare('SELECT * FROM articles WHERE title LIKE ?');
$stmt->execute([$keyword]);

MySQLi 預處理 (面向過程/物件)

<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'test');
$mysqli->set_charset('utf8mb4');

// 物件風格
$stmt = $mysqli->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->bind_param('s', $email); // 's'=string, 'i'=int, 'd'=double, 'b'=blob
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) { /* ... */ }
$stmt->close();

參數化查詢原理:為什麼能防注入?

  1. 編譯分離:SQL 模板 (`SELECT * FROM t WHERE c = ?`) 先發送給數據庫服務端**解析、編譯、優化、生成執行計劃**。此時參數 `?` 視為未知值,**不參與語法分析**。
  2. 2. 參數傳輸:執行時通過二進制協議傳輸參數值,**數據與代碼徹底隔離**,數據庫引擎直接將值填入執行計劃對應位置。 3. 無語法注入可能:即使用戶輸入 `' OR '1'='1`,也被視為完整字符串字面量值,而非 SQL 片段。

邊界情況與補充防禦

1. 動態標識符 (表名、列名、排序方向) - 無法參數化

<?php
// ❌ 錯誤:無法綁定表名/列名
// $stmt = $pdo->prepare("SELECT * FROM ? WHERE ? = ?"); // 報錯

// ✅ 正確:白名單驗證 + 反引號包裹
$allowedTables = ['users', 'orders', 'products'];
$allowedColumns = ['id', 'name', 'email', 'created_at'];

$table = in_array($_GET['table'], $allowedTables) ? $_GET['table'] : 'users';
$col = in_array($_GET['sort'], $allowedColumns) ? $_GET['sort'] : 'id';
$dir = ($_GET['dir'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';

$sql = sprintf('SELECT * FROM `%s` ORDER BY `%s` %s', $table, $col, $dir);
// $sql 現在安全,因為標識符來自白名單
$stmt = $pdo->query($sql);

2. `IN` 子句動態參數數量

<?php
$ids = [1, 5, 12, 99]; // 動態數組
$placeholders = implode(',', array_fill(0, count($ids), '?')); // ?,?,?,?
$sql = "SELECT * FROM users WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute($ids);

3. 二階注入防禦

**原理**:惡意數據首次入庫時合法 (經參數化),二次業務中被拼接進 SQL 執行。
**防禦**:**所有**進入 SQL 的變量**無一例外**使用參數化查詢,包括來自數據庫的讀取值。

4. 存儲過程/動態 SQL (應用層無法參數化)

避免在存儲過程中拼接動態 SQL。若必須,數據庫層面使用 `PREPARE`/`EXECUTE` 或 `sp_executesql` (SQL Server) 參數化。

框架/ORM 內置防護 (開箱即用)

框架ORM/查詢構建器防注入機制
LaravelEloquent / DB::table()所有 `where`/`whereIn`/`update`/`insert` 內部統一 PDO 參數化
SymfonyDoctrine DQL / QueryBuilderDQL 編譯為參數化 SQL;原生 SQL 需手動 `setParameter`
ThinkPHP 6/8Db::name() / Model查詢構建器自動參數化;`whereRaw` 需謹慎
CodeIgniter 3/4Query Builder`$this->db->where()` 參數化;`query()` 需手動 `$this->db->escape()`
Yii 2ActiveRecord / Query參數化綁定;`where(['col' => $val])` 安全

安全開發清單 (Code Review 必查)

[ ] 所有 SQL 執行路徑使用 prepare()/bindParam()/execute()
[ ] 無字符串拼接 SQL (包括 . 拼接、雙引號內變量、sprintf、vsprintf)
[ ] 動態表名/列名/排序/分頁參數經過白名單驗證
[ ] IN 子句動態生成佔位符
[ ] 數據庫連接字符集 utf8mb4 (防多字節繞過)
[ ] 錯誤信息不回顯給前端 (生產關閉 display_errors、PDO::ERRMODE_EXCEPTION 記錄日誌)
[ ] 最小權限數據庫賬號 (僅 SELECT/INSERT/UPDATE/DELETE,無 DROP/ALTER/GRANT)
[ ] WAF/防火牆部署 (ModSecurity + OWASP CRS) 作為第二道防線
[ ] 定期 SQL 注入掃描 (sqlmap 自動化測試)
[ ] 代碼審計工具集成 (PHPStan + phpstan-security, Psalm, SonarQube)

常見誤區辨析

誤區真相
「我用 `addslashes` 就安全了」GBK 編碼下 `0xbf27` 吃掉反斜杠,繞過轉義。參數化才是銀彈。
「PDO 模擬預處理 (`ATTR_EMULATE_PREPARES=true`) 也安全」客戶端模擬轉義,**不如服務端真實預處理安全**,邊界情況可能繞過。**務必設置 `false`**。
「ORM 就絕對安全」`whereRaw`、`expr`、`orderByRaw`、`havingRaw` 等原生方法**不自動參數化**,需手動綁定。
「轉義函數能防禦所有注入」無法防禦二階注入、標識符注入、寬字節注入、邊界繞過。
「WAF 能攔截所有攻擊」WAF 基於特徵庫,存在繞過 (編碼、大小寫、註釋、HTTP 分塊)。**代碼層面參數化是根本**。

鐵律:**「參數化查詢」是 SQL 注入的唯一標準答案。** 任何手動轉義、拼接、過濾、WAF 依賴均為危險補丁。新項目從第一行代碼開始強制參數化,舊項目重構優先清理拼接 SQL。數據庫賬號最小權限、錯誤不回顯、定期掃描,構建纵深防禦。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved