1. PHP应用中的SQL注入基础原理
SQL注入是Web安全领域最常见也最危险的漏洞之一。在PHP应用中,当开发者直接将用户输入拼接到SQL查询语句中时,攻击者就能通过精心构造的输入改变原始SQL语义。我见过太多因为一个简单的字符串拼接导致的数据库泄露案例。
PHP中最典型的危险写法是这样的:
php复制$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
当攻击者传入1 OR 1=1时,查询就变成了SELECT * FROM users WHERE id = 1 OR 1=1,这将返回所有用户数据。更危险的情况是攻击者使用联合查询获取其他表数据,或者通过;执行多条SQL语句。
注意:MySQL默认配置下不支持多语句执行,但SQL Server/PostgreSQL等数据库支持,这种差异常被开发者忽视。
2. 符号拼接与特殊字符处理
PHP开发中处理SQL注入时,符号拼接方式直接影响防御效果。常见的三种处理方式:
- 直接拼接(高危):
php复制$name = $_POST['name'];
$sql = "SELECT * FROM products WHERE name LIKE '%$name%'";
- addslashes转义(不完全安全):
php复制$name = addslashes($_POST['name']);
// 输入 admin' or '1'='1 会被转义为 admin\' or \'1\'=\'1
- 预处理语句(推荐):
php复制$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ?");
$stmt->execute(["%".$_POST['name']."%"]);
特殊字符的处理需要特别注意:
- 单引号(')和双引号("):最基础的注入点
- 注释符(--, #, /* */):用于截断后续SQL
- 分号(;):尝试执行多条语句
- 反斜杠():转义字符本身可能被利用
3. 不同请求方法下的注入防护
3.1 GET请求注入
GET参数直接暴露在URL中,是最容易被发现的注入点。典型攻击方式:
code复制/product.php?id=1' UNION SELECT 1,username,password FROM users--
防御要点:
- 永远不要相信URL参数
- 使用filter_input过滤:
php复制$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false) die('Invalid ID');
3.2 POST请求注入
POST数据虽然不可见,但通过Burp Suite等工具仍可修改。表单处理常见错误:
php复制$login = "SELECT * FROM users WHERE username='$_POST[user]' AND password='$_POST[pass]'";
安全做法:
php复制$stmt = $pdo->prepare("SELECT * FROM users WHERE username=? AND password=?");
$stmt->execute([$_POST['user'], hash('sha256', $_POST['pass'])]);
3.3 JSON请求注入
现代API常接收JSON数据,开发者容易忽略其安全性:
php复制$data = json_decode(file_get_contents('php://input'), true);
$sql = "INSERT INTO logs (message) VALUES ('{$data['msg']}')";
攻击者可构造:
json复制{"msg":"test'); DROP TABLE logs;--"}
防护方案:
php复制$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare("INSERT INTO logs (message) VALUES (?)");
$stmt->execute([$data['msg']]);
4. HTTP头注入与防护
4.1 常见注入点
- User-Agent:
$_SERVER['HTTP_USER_AGENT'] - X-Forwarded-For:
$_SERVER['HTTP_X_FORWARDED_FOR'] - Referer:
$_SERVER['HTTP_REFERER']
危险案例:
php复制$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$sql = "INSERT INTO visits (ip) VALUES ('$ip')";
攻击者可设置请求头:
code复制X-Forwarded-For: 1.1.1.1', (SELECT GROUP_CONCAT(username,':',password) FROM users))--
4.2 防护措施
- 过滤所有HTTP头变量
- 使用特定函数获取客户端IP:
php复制function getClientIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
return filter_var($ip, FILTER_VALIDATE_IP);
}
5. 编码类与二次注入防御
5.1 常见编码方式
- URL编码:
%27代替单引号 - Hex编码:
0x61646D696E代替'admin' - Unicode编码:
\u0027代替单引号
5.2 二次注入场景
数据存入时转义,但取出使用时未转义:
php复制// 存入时
$username = $pdo->quote($_POST['username']); // 转义输入
$pdo->exec("INSERT INTO users (username) VALUES ($username)");
// 取出使用时
$user = $pdo->query("SELECT username FROM users WHERE id = 1")->fetch();
$sql = "SELECT * FROM profiles WHERE username = '{$user['username']}'";
如果最初存入的是admin'--,虽然第一次转义安全,但取出后直接使用仍会导致注入。
5.3 全面防御方案
- 预处理语句处理所有动态值
- 最小权限原则配置数据库用户
- 使用ORM框架自动处理参数
- 定期安全扫描与代码审计
6. 实战:DVWA靶场SQL注入分析
以DVWA的SQL Injection关卡为例,演示完整攻击链:
- 探测注入点:
code复制id=1' AND 1=1--
id=1' AND 1=2--
- 确定字段数:
code复制id=1' ORDER BY 2--
id=1' ORDER BY 3-- // 报错说明只有2列
- 联合查询获取数据:
code复制id=1' UNION SELECT user(), database()--
id=1' UNION SELECT table_name, 2 FROM information_schema.tables WHERE table_schema=database()--
防御代码对比:
php复制// 不安全代码
$id = $_GET['id'];
$result = $pdo->query("SELECT first_name, last_name FROM users WHERE user_id = $id");
// 安全代码
$stmt = $pdo->prepare("SELECT first_name, last_name FROM users WHERE user_id = ?");
$stmt->execute([$_GET['id']]);
7. PHP安全编码最佳实践
- 输入验证:
php复制// 验证整数
if (!filter_var($id, FILTER_VALIDATE_INT)) die('Invalid ID');
// 验证邮箱
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) die('Invalid Email');
- 预处理语句:
php复制$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
$stmt->execute([$name, $email]);
- 最小权限原则:
- 应用数据库用户只赋予必要权限
- 禁止GRANT, FILE, PROCESS等敏感权限
- 错误处理:
php复制// 生产环境关闭错误显示
ini_set('display_errors', 0);
// 记录到安全日志
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
- 定期更新:
- 保持PHP版本最新
- 更新所有依赖库
- 使用Composer管理依赖
在最近的一次安全审计中,我发现很多团队虽然知道预处理语句的重要性,但在复杂查询场景下仍会退回到字符串拼接。我的建议是:无论多复杂的SQL,都应该坚持使用参数化查询。对于动态表名/列名等情况,可以通过白名单验证处理,而不是直接拼接。
