本文整理了多種 PHP 防 SQL 注入類代碼,涵蓋整型/字符串參數驗證、批量過濾、關鍵字匹配等方案,並指出歷史代碼的嚴重缺陷,提供現代最佳實踐指引。
if (@get_magic_quotes_gpc()) {
$_GET = sec($_GET);
$_POST = sec($_POST);
$_COOKIE = sec($_COOKIE);
$_FILES = sec($_FILES);
}
$_SERVER = sec($_SERVER);
function sec(&$array) {
if (is_array($array)) {
foreach ($array as $k => $v) {
$array[$k] = sec($v);
}
} else if (is_string($array)) {
$array = addslashes($array); // 危險!
} else if (is_numeric($array)) {
$array = intval($array);
}
return $array;
}
缺陷: 依賴已移除的 magic_quotes_gpc;addslashes 非 SQL 專用,GBK 等多字節編碼下可繞過;污染超全局變量導致數據不可逆損壞。
function num_check($id) {
if (!$id) { die('參數不能為空!'); }
elseif (inject_check($id)) { die('非法參數'); }
elseif (!is_numeric($id)) { die('非法參數'); } // 拼寫錯誤:is_numetic
$id = intval($id);
return $id;
}
function str_check($str) {
if (inject_check($str)) { die('非法參數'); }
$str = htmlspecialchars($str);
return $str;
}
function search_check($str) {
$str = str_replace("_", "_", $str); // 邏輯錯誤:替換為自身
$str = str_replace("%", "%", $str);
$str = htmlspecialchars($str);
return $str;
}
function post_check($str, $min, $max) {
if (isset($min) && strlen($str) < $min) { die('最少$min字節'); } // 變量未解析
if (isset($max) && strlen($str) > $max) { die('最多$max字節'); }
return stripslashes_array($str);
}
function inject_check($sql_str) {
return eregi('select|inert|update|delete|\'|/\*|\*|../|./|UNION|into|load_file|outfile', $sql_str);
// 拼寫錯誤:inert → insert
// 邏輯錯誤:返回 true 表示有注入,但調用處邏輯混亂
}
致命問題:
eregi 已移除'|/\*|\*|../|./ 句法無效if (get_magic_quotes_gpc()) {
$_GET = stripslashes_array($_GET);
$_POST = stripslashes_array($_POST);
}
function stripslashes_array(&$array) {
while (list($key, $var) = each($array)) { // each() 已棄用
if ($key != 'argc' && $key != 'argv' && (strtoupper($key) != $key || ''.intval($key) == "$key")) {
if (is_string($var)) {
$array[$key] = stripslashes($var);
}
if (is_array($var)) {
$array[$key] = stripslashes_array($var);
}
}
}
return $array;
}
function htmlencode($str) {
// ... 大量 str_replace 邏輯錯誤:
$str = str_replace("&", "&", $str); // 無意義自替換
$str = str_replace(">", ">", $str);
$str = str_replace("<", "<", $str);
$str = str_replace(chr(32), " ", $str); // 空格替換為空格
$str = str_replace(chr(9), " ", $str); // 制表符替換為空格
$str = str_replace(chr(9), " ", $str); // 覆蓋上一行
$str = str_replace(chr(34), "&", $str); // 雙引號替換為 & 錯誤
$str = str_replace(chr(39), "'", $str); // 單引號替換為 '
// ... 更多邏輯錯誤
$str = str_replace("select", "select", $str); // 關鍵字自替換,無任何作用
return $str;
}
從 common_list.txt、signify_list.txt 讀取關鍵字,支持精確/模糊匹配。缺點:維護成本高、性能差、無法防禦編碼繞過、誤殺率高。
<?php
class Database {
private PDO $pdo;
public function __construct(string $dsn, string $user, string $pass) {
$this->pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 關鍵:使用真實預處理
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
// 安全查詢:自動參數綁定
public function query(string $sql, array $params = []): PDOStatement {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public function fetchOne(string $sql, array $params = []): ?array {
return $this->query($sql, $params)->fetch() ?: null;
}
public function fetchAll(string $sql, array $params = []): array {
return $this->query($sql, $params)->fetchAll();
}
public function execute(string $sql, array $params = []): int {
return $this->query($sql, $params)->rowCount();
}
public function lastInsertId(): string {
return $this->pdo->lastInsertId();
}
}
// 使用示例
$db = new Database('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
// 整型參數:業務層驗證 + 預處理
function getUserById(Database $db, int $id): ?array {
if ($id <= 0) throw new InvalidArgumentException('無效 ID');
return $db->fetchOne('SELECT * FROM users WHERE id = ? AND status = 1', [$id]);
}
// 字符串參數:長度/格式驗證 + 預處理
function searchArticles(Database $db, string $keyword, int $page = 1, int $size = 20): array {
$keyword = trim($keyword);
if ($keyword === '') throw new InvalidArgumentException('關鍵詞不能為空');
if (mb_strlen($keyword) > 50) throw new InvalidArgumentException('關鍵詞過長');
$offset = ($page - 1) * $size;
return $db->fetchAll(
'SELECT * FROM articles WHERE title LIKE ? LIMIT ?, ?',
["%{$keyword}%", $offset, $size]
);
}
?>
<?php
// 簡易驗證器示例
class Validator {
public static function id(mixed $value, string $field = 'ID'): int {
$value = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if ($value === false) throw new InvalidArgumentException("{$field} 必須為正整數");
return $value;
}
public static function string(mixed $value, int $maxLen = 255, string $field = '字段'): string {
$value = (string)$value;
if (mb_strlen($value) > $maxLen) throw new InvalidArgumentException("{$field} 長度不能超過 {$maxLen}");
return $value;
}
public static function email(mixed $value): string {
$value = (string)$value;
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) throw new InvalidArgumentException('郵箱格式無效');
return $value;
}
public static function regex(mixed $value, string $pattern, string $field = '字段'): string {
$value = (string)$value;
if (!preg_match($pattern, $value)) throw new InvalidArgumentException("{$field} 格式不符合要求");
return $value;
}
}
// 控制器層使用
$userId = Validator::id($_GET['id'] ?? 0);
$email = Validator::email($_POST['email'] ?? '');
$phone = Validator::regex($_POST['phone'], '/^1[3-9]\d{9}$/', '手機號');
?>
| 場景 | 方案 |
|---|---|
| 動態表名/列名 | 白名單驗證:in_array($table, $allowedTables),絕不拼接 |
| IN 子句數組參數 | 生成佔位符:implode(',', array_fill(0, count($ids), '?')) |
| LIKE 模糊查詢 | 手動轉義通配符:str_replace(['%', '_'], ['\%', '\_'], $keyword) 後加 % |
| 多條件動態構建 | 收集條件數組與參數數組,最後 implode(' AND ', $conditions) |
| 存儲過程調用 | 使用 CALL proc(?, ?) 同樣參數綁定 |
mysql_query、mysqli_query、->query( 非預處理調用addslashes、mysql_real_escape_string、inject_check 等過濾函數magic_quotes_gpc 相關邏輯本文收集的歷史代碼普遍存在:依賴棄用功能、黑名單邏輯缺陷、正則錯誤、邏輯自相矛盾等嚴重問題,**絕不可用於生產環境**。現代 PHP 防 SQL 注入的唯一正解是:**預處理語句作為核心防線,輸入驗證作為業務約束,最小權限作為纵深防禦**。請立即遷移至 PDO/MySQLi 預處理架構。