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輸入 myuser' or 'foo' = 'foo' -- 作為帳號,SQL 變為:
SELECT * FROM users
WHERE username = 'myuser' or 'foo' = 'foo' --
AND password = '...'
-- 註解掉後續密碼驗證,'foo' = 'foo' 恆為真,繞過認證直接登入。
根據資料類型嚴格驗證:
<?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('帳號格式錯誤');
}
?>
使用 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($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);
// 更新資料庫
}
?>
舊版 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);
}
?>
SQL 注入並非不可防禦,關鍵在於建立「不信任任何輸入」的開發文化,並將參數化查詢內化為肌肉記憶。