SQL注入防护
什么是SQL注入
SQL注入(SQL Injection)是一种代码注入技术,攻击者通过在应用程序的输入字段中插入恶意的SQL代码,来操纵或破坏数据库。这是最常见和最危险的Web应用安全漏洞之一。
SQL注入的危害
- 数据泄露:攻击者可以窃取敏感数据
- 数据篡改:修改或删除数据库中的数据
- 权限提升:获得管理员权限
- 系统控制:在某些情况下可以控制整个服务器
SQL注入的基本原理
1. 不安全的查询构建
<?php
// 危险的代码示例 - 容易受到SQL注入攻击
$userId = $_GET['id'];
// 直接将用户输入拼接到SQL语句中
$sql = "SELECT * FROM users WHERE id = $userId";
$result = mysqli_query($conn, $sql);
?>
2. 攻击示例
<?php
// 正常请求
// URL: user.php?id=1
// 生成的SQL: SELECT * FROM users WHERE id = 1
// 恶意请求
// URL: user.php?id=1 OR 1=1
// 生成的SQL: SELECT * FROM users WHERE id = 1 OR 1=1
// 结果:返回所有用户数据
// 更危险的攻击
// URL: user.php?id=1; DELETE FROM users; --
// 生成的SQL: SELECT * FROM users WHERE id = 1; DELETE FROM users; --
// 结果:删除整个users表
?>
SQL注入的常见类型
1. 基于UNION的注入
<?php
// 攻击者利用UNION查询获取其他表的数据
// 输入: 1 UNION SELECT username, password FROM admin
$userId = $_GET['id'];
$sql = "SELECT name, email FROM users WHERE id = $userId";
// 结果:同时查询admin表的用户名和密码
?>
2. 基于布尔的盲注
<?php
// 攻击者通过真假条件逐个猜测数据
// 输入: 1 AND (SELECT SUBSTRING(password,1,1) FROM admin WHERE id=1)='a'
$userId = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $userId";
// 通过返回结果判断条件真假
?>
3. 基于时间的盲注
<?php
// 利用数据库函数延迟响应来判断条件
// 输入: 1 AND IF((SELECT password FROM admin WHERE id=1) LIKE 'a%', SLEEP(5), 0)
$userId = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $userId";
?>
SQL注入防护方法
1. 使用预处理语句(Prepared Statements)
预处理语句是防止SQL注入最有效的方法。
使用PDO预处理语句
<?php
class Database {
private $pdo;
public function __construct($host, $dbname, $username, $password) {
try {
$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8mb4";
$this->pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
}
// 安全的用户查询
public function getUserById($id) {
$sql = "SELECT * FROM users WHERE id = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$id]);
return $stmt->fetch();
}
// 安全的用户登录
public function loginUser($username, $password) {
$sql = "SELECT * FROM users WHERE username = ? AND password = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$username, $password]);
return $stmt->fetch();
}
// 安全的插入操作
public function insertUser($username, $email, $password) {
$sql = "INSERT INTO users (username, email, password) VALUES (?, ?, ?)";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([$username, $email, $password]);
}
// 安全的更新操作
public function updateUser($id, $username, $email) {
$sql = "UPDATE users SET username = ?, email = ? WHERE id = ?";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([$username, $email, $id]);
}
// 安全的删除操作
public function deleteUser($id) {
$sql = "DELETE FROM users WHERE id = ?";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([$id]);
}
}
// 使用示例
$db = new Database('localhost', 'myapp', 'user', 'pass');
// 安全的查询
$userId = $_GET['id'];
$user = $db->getUserById($userId);
if ($user) {
echo "用户名: " . htmlspecialchars($user['username']);
} else {
echo "用户不存在";
}
?>
使用mysqli预处理语句
<?php
class MySQLiDB {
private $conn;
public function __construct($host, $username, $password, $dbname) {
$this->conn = new mysqli($host, $username, $password, $dbname);
if ($this->conn->connect_error) {
die("连接失败: " . $this->conn->connect_error);
}
// 设置字符集
$this->conn->set_charset("utf8mb4");
}
// 安全的查询方法
public function query($sql, $params = []) {
$stmt = $this->conn->prepare($sql);
if ($stmt === false) {
die("预处理失败: " . $this->conn->error);
}
if (!empty($params)) {
// 动态绑定参数
$types = str_repeat('s', count($params));
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
return $stmt;
}
// 获取单行数据
public function fetchOne($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->get_result()->fetch_assoc();
}
// 获取多行数据
public function fetchAll($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->get_result()->fetch_all(MYSQLI_ASSOC);
}
// 插入数据
public function insert($table, $data) {
$columns = array_keys($data);
$placeholders = array_fill(0, count($columns), '?');
$values = array_values($data);
$sql = "INSERT INTO $table (" . implode(', ', $columns) . ")
VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $this->query($sql, $values);
return $stmt->insert_id;
}
}
// 使用示例
$db = new MySQLiDB('localhost', 'user', 'pass', 'myapp');
// 安全的登录验证
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT id, username, role FROM users WHERE username = ? AND password = ?";
$user = $db->fetchOne($sql, [$username, $password]);
if ($user) {
session_start();
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
header('Location: dashboard.php');
} else {
echo "用户名或密码错误";
}
?>
2. 输入验证和过滤
<?php
class InputValidator {
// 验证ID(必须是正整数)
public static function validateId($input) {
if (filter_var($input, FILTER_VALIDATE_INT) && $input > 0) {
return (int)$input;
}
throw new InvalidArgumentException("无效的ID");
}
// 验证用户名(字母数字下划线,3-20字符)
public static function validateUsername($input) {
if (preg_match('/^[a-zA-Z0-9_]{3,20}$/', $input)) {
return $input;
}
throw new InvalidArgumentException("用户名只能包含字母、数字和下划线,长度3-20字符");
}
// 验证邮箱
public static function validateEmail($input) {
if (filter_var($input, FILTER_VALIDATE_EMAIL)) {
return $input;
}
throw new InvalidArgumentException("无效的邮箱地址");
}
// 验证字符串长度
public static function validateLength($input, $min, $max) {
$length = mb_strlen($input, 'UTF-8');
if ($length >= $min && $length <= $max) {
return $input;
}
throw new InvalidArgumentException("长度必须在{$min}-{$max}字符之间");
}
// 清理输入
public static function sanitize($input, $type = 'string') {
switch ($type) {
case 'int':
return filter_var($input, FILTER_SANITIZE_NUMBER_INT);
case 'float':
return filter_var($input, FILTER_SANITIZE_NUMBER_FLOAT);
case 'email':
return filter_var($input, FILTER_SANITIZE_EMAIL);
case 'url':
return filter_var($input, FILTER_SANITIZE_URL);
default:
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
}
}
// 使用示例
try {
$userId = InputValidator::validateId($_GET['id']);
$username = InputValidator::validateUsername($_POST['username']);
$email = InputValidator::validateEmail($_POST['email']);
// 验证通过后,使用预处理语句查询
$user = $db->fetchOne(
"SELECT * FROM users WHERE id = ? AND username = ? AND email = ?",
[$userId, $username, $email]
);
} catch (InvalidArgumentException $e) {
die("输入验证失败: " . $e->getMessage());
}
?>
3. 使用ORM(对象关系映射)
<?php
// 简单的ORM示例
class User {
private $db;
private $table = 'users';
public function __construct($database) {
$this->db = $database;
}
// 查找单个用户
public function find($id) {
$sql = "SELECT * FROM {$this->table} WHERE id = ?";
return $this->db->fetchOne($sql, [$id]);
}
// 根据条件查找
public function findBy($field, $value) {
$sql = "SELECT * FROM {$this->table} WHERE {$field} = ?";
return $this->db->fetchOne($sql, [$value]);
}
// 创建用户
public function create($data) {
// 验证必填字段
$required = ['username', 'email', 'password'];
foreach ($required as $field) {
if (empty($data[$field])) {
throw new InvalidArgumentException("字段 {$field} 是必填的");
}
}
return $this->db->insert($this->table, $data);
}
// 更新用户
public function update($id, $data) {
$fields = [];
$values = [];
foreach ($data as $field => $value) {
$fields[] = "{$field} = ?";
$values[] = $value;
}
$values[] = $id;
$sql = "UPDATE {$this->table} SET " . implode(', ', $fields) . " WHERE id = ?";
$stmt = $this->db->query($sql, $values);
return $stmt->affected_rows > 0;
}
// 删除用户
public function delete($id) {
$sql = "DELETE FROM {$this->table} WHERE id = ?";
$stmt = $this->db->query($sql, [$id]);
return $stmt->affected_rows > 0;
}
// 安全的搜索功能
public function search($keyword, $limit = 10, $offset = 0) {
// 使用LIKE进行安全搜索
$sql = "SELECT * FROM {$this->table}
WHERE username LIKE ? OR email LIKE ?
LIMIT ? OFFSET ?";
$pattern = "%{$keyword}%";
return $this->db->fetchAll($sql, [$pattern, $pattern, $limit, $offset]);
}
}
// 使用示例
$user = new User($db);
try {
// 安全的搜索
$keyword = InputValidator::sanitize($_GET['keyword'], 'string');
$users = $user->search($keyword);
// 安全的创建
$userData = [
'username' => InputValidator::sanitize($_POST['username']),
'email' => InputValidator::validateEmail($_POST['email']),
'password' => password_hash($_POST['password'], PASSWORD_DEFAULT)
];
$userId = $user->create($userData);
echo "用户创建成功,ID: {$userId}";
} catch (Exception $e) {
error_log("用户操作失败: " . $e->getMessage());
echo "操作失败,请稍后重试";
}
?>
检测SQL注入漏洞
1. 自动化检测工具
<?php
class SQLInjectionScanner {
private $url;
private $parameters;
private $errorPatterns;
public function __construct($url, $parameters = []) {
$this->url = $url;
$this->parameters = $parameters;
// 常见的SQL错误模式
$this->errorPatterns = [
"/SQL syntax.*MySQL/i",
"/Warning.*mysql_.*()/i",
"/valid MySQL result/i",
"/MySQLClient.*failed/i",
"/mysql_fetch_array\(\)/i",
"/mysql_num_rows\(\)/i",
"/ORA-[0-9]{5}/i",
"/Oracle error/i",
"/Microsoft OLE DB Provider for ODBC Drivers/i",
"/ODBC Microsoft Access Driver/i",
"/Microsoft JET Database/i",
"/SQLServer JDBC Driver/i",
"/PostgreSQL query failed/i",
/Warning.*pg_.*()/i",
"/Npgsql\./i"
];
}
// 检测SQL注入漏洞
public function scan() {
$results = [];
// 测试用例
$testPayloads = [
"'",
"\"",
"' OR '1'='1",
"\" OR \"1\"=\"1",
"' OR 1=1--",
"' UNION SELECT NULL--",
"'; DROP TABLE users; --",
"' AND (SELECT COUNT(*) FROM information_schema.tables)>0--"
];
foreach ($testPayloads as $payload) {
$result = $this->testPayload($payload);
if ($result['vulnerable']) {
$results[] = $result;
}
}
return $results;
}
private function testPayload($payload) {
// 构建测试URL
$testParams = [];
foreach ($this->parameters as $param => $value) {
$testParams[$param] = $payload;
}
$testUrl = $this->url . '?' . http_build_query($testParams);
// 发送请求
$response = @file_get_contents($testUrl);
if ($response === false) {
return ['payload' => $payload, 'vulnerable' => false, 'reason' => 'Request failed'];
}
// 检查响应中的SQL错误
foreach ($this->errorPatterns as $pattern) {
if (preg_match($pattern, $response)) {
return [
'payload' => $payload,
'vulnerable' => true,
'reason' => 'SQL error detected in response',
'url' => $testUrl
];
}
}
// 检查响应时间(时间盲注)
$start = microtime(true);
$slowPayload = "' AND (SELECT SLEEP(5))--";
$slowUrl = $this->url . '?' . http_build_query(array_fill_keys(array_keys($this->parameters), $slowPayload));
@file_get_contents($slowUrl);
$duration = microtime(true) - $start;
if ($duration > 4) {
return [
'payload' => $payload,
'vulnerable' => true,
'reason' => 'Possible time-based blind SQL injection',
'response_time' => $duration
];
}
return ['payload' => $payload, 'vulnerable' => false];
}
}
// 使用示例
$scanner = new SQLInjectionScanner(
'http://example.com/search.php',
['id' => '1', 'category' => 'books']
);
$vulnerabilities = $scanner->scan();
if (!empty($vulnerabilities)) {
echo "发现SQL注入漏洞:\n";
foreach ($vulnerabilities as $vuln) {
echo "- 载荷: {$vuln['payload']}\n";
echo " 原因: {$vuln['reason']}\n";
if (isset($vuln['url'])) {
echo " URL: {$vuln['url']}\n";
}
echo "\n";
}
} else {
echo "未发现明显的SQL注入漏洞";
}
?>
2. 日志监控
<?php
class SQLInjectionMonitor {
private $logFile;
private $suspiciousPatterns;
public function __construct($logFile = 'sql_injection.log') {
$this->logFile = $logFile;
// 可疑的SQL注入模式
$this->suspiciousPatterns = [
'/union\s+select/i',
'/select\s+.*\s+from\s+information_schema/i',
'/drop\s+table/i',
'/delete\s+from/i',
'/insert\s+into/i',
'/update\s+.*\s+set/i',
'/exec\s*\(/i',
'/script\s*>/i',
'/or\s+1\s*=\s*1/i',
'/and\s+1\s*=\s*1/i',
'/\'\s*or\s*\'.*\'.*\'/i',
'/\"\s*or\s*\".*\".*\"/i'
];
}
// 监控请求参数
public function monitorRequest() {
$allInputs = array_merge($_GET, $_POST, $_COOKIE);
foreach ($allInputs as $key => $value) {
if ($this->isSuspicious($value)) {
$this->logSuspiciousActivity($key, $value);
}
}
}
// 检查是否可疑
private function isSuspicious($input) {
if (is_array($input)) {
foreach ($input as $value) {
if ($this->isSuspicious($value)) {
return true;
}
}
return false;
}
foreach ($this->suspiciousPatterns as $pattern) {
if (preg_match($pattern, $input)) {
return true;
}
}
return false;
}
// 记录可疑活动
private function logSuspiciousActivity($parameter, $value) {
$logEntry = [
'timestamp' => date('Y-m-d H:i:s'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
'parameter' => $parameter,
'value' => $value
];
$logMessage = json_encode($logEntry, JSON_UNESCAPED_UNICODE) . "\n";
file_put_contents($this->logFile, $logMessage, FILE_APPEND | LOCK_EX);
// 可以添加实时告警逻辑
$this->sendAlert($logEntry);
}
// 发送告警
private function sendAlert($activity) {
// 邮件告警
$to = 'admin@example.com';
$subject = 'SQL注入攻击告警';
$message = "检测到可疑的SQL注入尝试:\n\n";
$message .= "时间: {$activity['timestamp']}\n";
$message .= "IP: {$activity['ip']}\n";
$message .= "参数: {$activity['parameter']}\n";
$message .= "值: {$activity['value']}\n";
$message .= "URL: {$activity['request_uri']}\n";
// 在生产环境中应该使用更安全的邮件发送方式
// mail($to, $subject, $message);
// 记录到系统日志
error_log("SQL注入攻击检测: " . json_encode($activity));
}
}
// 在每个页面请求开始时进行监控
$monitor = new SQLInjectionMonitor();
$monitor->monitorRequest();
?>
最佳实践总结
1. 永远不要相信用户输入
<?php
// 错误的做法
$sql = "SELECT * FROM users WHERE username = '" . $_POST['username'] . "'";
// 正确的做法
$sql = "SELECT * FROM users WHERE username = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST['username']]);
?>
2. 使用最小权限原则
<?php
// 为应用创建专用数据库用户,只授予必要权限
// 应用用户(只能读写特定表)
// GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.users TO 'app_user'@'localhost';
// 避免使用root权限连接数据库
?>
3. 错误处理
<?php
// 不要向用户显示详细的数据库错误
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetchAll();
} catch (PDOException $e) {
// 记录到日志
error_log("Database error: " . $e->getMessage());
// 向用户显示友好的错误信息
die("系统暂时繁忙,请稍后重试");
}
?>
4. 使用框架的安全功能
<?php
// 如果使用框架(如Laravel),充分利用其ORM和查询构建器
// Laravel示例 - 安全的查询
$users = DB::table('users')
->where('active', 1)
->where('created_at', '>', $date)
->get();
// Eloquent示例 - 更安全的模型操作
$user = User::where('email', $request->email)->first();
?>
通过学习这些SQL注入防护技术,你可以构建更加安全的PHP应用程序,保护用户数据免受恶意攻击。记住,安全是一个持续的过程,需要时刻保持警惕。