SQL 注入是 Web 應用最嚴重的安全風險之一。本文從伺服器層面設定、程式碼層面防護、現代最佳實務三個維度完整說明。
| 設定項 | 建議值 | 說明 |
|---|---|---|
| safe_mode | On | 限制危險函數、檔案存取(PHP 5.4+ 已移除) |
| safe_mode_gid | Off | 允許同群組存取 |
| open_basedir | /var/www | 限制腳本可存取目錄 |
| disable_functions | exec,shell_exec,system,passthru,popen,proc_open,phpinfo | 停用命令執行函數 |
| expose_php | Off | 隱藏 PHP 版本資訊 |
| register_globals | Off | 關閉自動全域變數(PHP 5.4+ 預設關閉) |
| magic_quotes_gpc | On | 自動跳脫輸入(PHP 5.4+ 已移除,不建議依賴) |
| display_errors | Off | 生產環境關閉錯誤顯示 |
| log_errors | On | 啟用錯誤記錄 |
| error_log | /var/log/php_error.log | 錯誤日誌路徑 |
<?php
// 錯誤:未初始化直接判斷
if ($admin) { include('admin.php'); }
// 正確:明確初始化
$admin = 0;
if ($_POST['user'] && $_POST['pass']) {
// 驗證邏輯
$admin = 1;
}
if ($admin) { include('admin.php'); }
?>
<?php
/** SQL 關鍵字檢查 */
function inject_check($str) {
return preg_match('/select|insert|update|delete|\'|\\*|union/i', $str);
}
/** ID 參數驗證 */
function verify_id($id = null) {
if (!$id) exit('缺少參數');
if (inject_check($id)) exit('非法參數');
if (!is_numeric($id)) exit('非法參數');
return intval($id);
}
// 使用
$id = verify_id($_GET['id']);
$sql = "SELECT * FROM news WHERE id = $id";
?>
<?php
function str_check($str) {
// magic_quotes_gpc 已棄用,建議統一手動處理
if (!get_magic_quotes_gpc()) {
$str = addslashes($str); // 單位元組字串
}
// SQL LIKE 萬用字元跳脫
$str = str_replace('_', '\\_', $str);
$str = str_replace('%', '\\%', $str);
return $str;
}
function post_check($post) {
$post = str_check($post);
$post = nl2br($post); // 換行轉 <br>
$post = htmlspecialchars($post); // HTML 實體編碼
return $post;
}
?>
addslashes()、mysql_real_escape_string() 皆非萬靈丹(多位元組繞過、編碼問題)。唯一正確做法:參數化查詢 / 預處理語句。
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 關鍵:使用真正預處理
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// 查詢
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute([':email' => $email, ':status' => 1]);
$user = $stmt->fetch();
// 新增
$stmt = $pdo->prepare('INSERT INTO users (name, email, created_at) VALUES (:name, :email, NOW())');
$stmt->execute([':name' => $name, ':email' => $email]);
?id = $pdo->lastInsertId();
?>
<?php
$mysqli = new mysqli('127.0.0.1', 'user', 'pass', 'test');
$mysqli->set_charset('utf8mb4');
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
$stmt->close();
?>
display_errors,啟用 log_errorsopen_basedir 限制存取範圍disable_functions 停用危險函數