=========================
SQL 注入长期位居 OWASP Top 10 前列,而 MySQL 作为全球使用最广泛的开源数据库,自然是攻击者的重点目标。很多开发者对 SQL 注入的理解仍停留在“过滤单引号”的层面,实际上现代注入手法早已覆盖报错注入、盲注、堆叠注入、二次注入等多种路径。本文从代码层面出发,梳理 MySQL 场景下防止 SQL 注入的几种主流写法,并对比其适用边界。
一、理解 SQL 注入的成因
SQL 注入的本质是用户输入被当作 SQL 代码执行。例如:
$sql = "SELECT * FROM users WHERE name = '$_GET[name]'";
当 name 传入 ' OR '1'='1 时,语句逻辑被篡改,攻击者可绕过认证甚至拖库。要根治这个问题,核心原则只有一条:让数据永远保持为数据,不能进入代码层。
二、预处理语句(Prepared Statement)
这是目前最推荐的方案。预处理语句将 SQL 模板与参数分离,数据库先编译模板,再绑定数据,参数不会被解析为 SQL 语法。
1. PDO 写法(PHP)
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name AND age > :age');
$stmt->execute([':name' => $name, ':age' => $age]);
$rows = $stmt->fetchAll();
注意:PDO::ATTR_EMULATE_PREPARES 建议设为 false,使用 MySQL 原生预处理,避免模拟预处理在特殊字符集下被绕过。
2. mysqli 写法
$stmt = $mysqli->prepare('SELECT * FROM users WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();
3. Java / JDBC
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
4. Python / PyMySQL
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
需要特别提醒:表名、列名、ORDER BY 字段不能作为绑定参数。如果业务需要动态排序,必须使用白名单校验:
$allowed = ['id', 'name', 'created_at'];
$order = in_array($_GET['order'], $allowed) ? $_GET['order'] : 'id';
三、参数化查询的常见误区
不少团队以为用了 ORM 就安全,其实不然:
- 字符串拼接 ORM 原生 SQL:
Model::whereRaw("name = '$name'")依然存在注入。 - LIKE 查询:
WHERE name LIKE '%?%'是无效写法,应写成WHERE name LIKE CONCAT('%', ?, '%')。 - IN 子句:不能直接绑定数组,需要根据元素个数动态生成占位符。
- 二次注入:数据先存入数据库,后续取出再拼接进 SQL,预处理也救不了,必须在每次拼接处都遵循参数化原则。
四、输入验证与白名单
参数化并非万能,遇到无法绑定的场景(如动态表名、LIMIT 数值),白名单是最后一道防线:
$table = in_array($t, ['users', 'orders']) ? $t : 'users';
$limit = max(1, min((int)$_GET['limit'], 100));
对于数值型参数,强制类型转换 (int)、(float) 通常足够;对于字符串,使用 preg_match 限定字符集:
if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $username)) {
throw new InvalidArgumentException('非法用户名');
}
五、转义函数:兜底而非首选
mysqli_real_escape_string() 和 PDO::quote() 可以对特殊字符转义,但它们依赖字符集设置,若连接字符集与数据库不一致(如 GBK 宽字节注入),仍可能被绕过。因此转义只应作为历史代码的过渡手段,新项目一律使用预处理。
六、最小权限原则
即使代码存在漏洞,合理的数据库权限也能限制损失:
- 应用账号只授予
SELECT / INSERT / UPDATE / DELETE,禁止DROP、FILE、GRANT。 - 禁止应用账号访问
mysql、information_schema等系统库(除非确有需要)。 - 不同业务使用不同数据库账号,避免一处失守全库沦陷。
CREATE USER 'app'@'10.0.0.%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app'@'10.0.0.%';
七、WAF 与日志监控
WAF 可以拦截常见注入特征(如 UNION SELECT、SLEEP(、INFORMATION_SCHEMA),但攻击者可通过编码、注释、大小写混写绕过,因此 WAF 只能作为纵深防御的一层,不能替代代码修复。同时建议开启 MySQL 慢查询日志和通用日志,对异常 SQL 做告警:
[mysqld]
general_log = ON
general_log_file = /var/log/mysql/general.log
八、总结
防止 SQL 注入的核心可以浓缩为一句话:永远不要拼接用户输入到 SQL 中。优先级排序如下:
- 预处理语句 + 参数绑定(首选)
- 白名单校验动态标识符
- 强制类型转换数值参数
- 最小权限数据库账号
- WAF 与日志监控作为补充
安全不是某个函数能解决的问题,而是编码习惯、架构设计与运维策略共同作用的结果。把预处理写进团队的代码规范,配合代码审计与自动化扫描,才能真正把 SQL 注入挡在门外。
未经允许不得转载:任鹏个人博客 » MySQL 防止 SQL 注入的几种写法


朋友圈点赞图在线生成源码