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

PHP SQL Injection 完整防範指南:從原理到實戰的纏鬥

     編輯:PHP 綜合知識

SQL 注入(SQL Injection)是 Web 應用程式最經典也最危險的漏洞之一。其成因單純:未過濾使用者輸入與未轉義資料庫輸出這兩個關鍵步驟缺一不可,必須同時落實。

攻擊者視角:如何一步步推導注入點

以簡單登入表單為例:

<form action="/login.php" method="POST">
  <p>Username: <input type="text" name="username" /></p>
  <p>Password: <input type="password" name="password" /></p>
  <p><input type="submit" value="Log In" /></p>
</form>

後端未防護的查詢:

<?php
$password_hash = md5($_POST['password']);
$sql = "SELECT count(*) FROM users 
        WHERE username = '{$_POST['username']}' 
        AND password = '$password_hash'";
?>

第一步:偵測注入點

攻擊者輸入單引號 ' 作為帳號,若後端直接拼接 SQL 並呼叫 mysql_error() 輸出錯誤:

You have an error in your SQL syntax near 'WHERE username = ''' AND password = '...'

即可確認:

  • 欄位名稱:username、password
  • 查詢結構:WHERE 條件順序
  • 無輸入過濾、無輸出轉義

第二步:構造惡意載荷

輸入 myuser' or 'foo' = 'foo' -- 作為帳號,SQL 變為:

SELECT * FROM users 
WHERE username = 'myuser' or 'foo' = 'foo' -- 
AND password = '...'

-- 註解掉後續密碼驗證,'foo' = 'foo' 恆為真,繞過認證直接登入。

核心防禦雙支柱

1. 過濾輸入 - 白名單驗證

根據資料類型嚴格驗證:

<?php
$clean = [];

// 整數 ID
$clean['id'] = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

// Email 格式
$clean['email'] = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);

// 字串:長度、字元集限制
$clean['username'] = substr(trim($_POST['username']), 0, 32);
if (!preg_match('/^[a-zA-Z0-9_]+$/', $clean['username'])) {
    die('帳號格式錯誤');
}
?>

2. 轉義輸出 - 參數化查詢(最推薦)

使用 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, // 使用真正的預備語句
]);

$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->execute([$clean['username'], $password_hash]);
$user = $stmt->fetch();
?>

優勢:資料永遠不會被解析為 SQL 指令片段,從根本杜絕注入可能。

備選方案:手動轉義(不推薦為主流)

若必須動態拼接 SQL,使用驅動程式專用轉義函數:

<?php
$clean['last_name'] = "O'Reilly";
$mysql['last_name'] = $pdo->quote($clean['last_name']); // PDO::quote()

$sql = "INSERT INTO user (last_name) VALUES ({$mysql['last_name']})";
$pdo->exec($sql);
?>

舊版 mysql_real_escape_string() 已棄用,addslashes() 不支援多位元組字元集,均不建議在新專案使用。

密碼儲存:MD5 已死,請用 bcrypt/Argon2

文章提到的 md5($salt . md5($password . $salt)) 在現代標準下極不安全。請直接使用 PHP 內建密碼雜湊 API:

<?php
// 註冊:雜湊密碼
$hash = password_hash($password, PASSWORD_DEFAULT); // 目前為 bcrypt
// 儲存 $hash 至資料庫

// 登入:驗證密碼
if (password_verify($input_password, $stored_hash)) {
    // 密碼正確
}

// 定期重新雜湊(演算法升級時)
if (password_needs_rehash($stored_hash, PASSWORD_DEFAULT)) {
    $new_hash = password_hash($password, PASSWORD_DEFAULT);
    // 更新資料庫
}
?>

關於 magic_quotes_gpc 的歷史包袱

舊版 PHP 預設開啟 magic_quotes_gpc 自動 addslashes(),造成雙重轉義混亂。PHP 5.4 已移除此功能。現代專案不應依賴此設定,應在啟動階段檢查並移除其影響:

<?php
if (get_magic_quotes_gpc()) {
    function stripslashes_deep($value) {
        return is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value);
    }
    $_GET    = stripslashes_deep($_GET);
    $_POST   = stripslashes_deep($_POST);
    $_COOKIE = stripslashes_deep($_COOKIE);
}
?>

總結:防禦清單

  • ✅ 全面採用 PDO + 預備語句
  • ✅ 白名單驗證所有外部輸入
  • ✅ 錯誤訊息不輸出給使用者,改寫日誌
  • ✅ 資料庫帳號最小權限原則(僅 SELECT/INSERT/UPDATE/DELETE)
  • ✅ 密碼使用 password_hash()/password_verify()
  • ✅ 定期進行滲透測試與代碼審查

SQL 注入並非不可防禦,關鍵在於建立「不信任任何輸入」的開發文化,並將參數化查詢內化為肌肉記憶。

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