MySQL注入漏洞防御,业务禁止拼接SQL语句强制预编译
MySQL注入漏洞防御的核心思路,是让SQL语句的结构与用户输入彻底分离。
所谓禁止拼接SQL语句,就是不要用字符串把用户参数直接接进SQL;
而强制预编译(预编译语句)则是把SQL模板先交给数据库解析,再把参数单独传入。
本文会从原理、代码示例、排查方法和验证步骤讲清楚。
为什么拼接SQL会成为注入漏洞?
只要用户输入被当成SQL代码的一部分,就会产生注入风险。
例如下面这段PHP代码:
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
如果id传入1 OR 1=1,实际执行的SQL就变成:
SELECT * FROM users WHERE id = 1 OR 1=1
这会绕过条件,把整张表的数据都查出来。
更极端的还可以用1; DROP TABLE users--直接删表。
结论:只要存在字符串拼接SQL,就存在可能的注入点,改造方向就是使用预编译。
强制预编译的正确写法:以PHP PDO为例
PDO的预处理写法如下:
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'user', 'pass', [
PDO::ATTR_EMULATE_PREPARES => false, // 关闭模拟预处理,使用MySQL本地预处理
]);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $id]);
$row = $stmt->fetch();
这里的SQL模板和参数分两次传给MySQL:数据库先解析模板,再把$id作为纯值绑定。
即使$id里包含SQL语句,也只会被当成字符串处理。PHP PDO 中设置ATTR_EMULATE_PREPARES => false,才能真正落到MySQL服务端预处理。
Java后端也需要用PreparedStatement
Java JDBC中,禁止使用Statement拼接字符串,应改用PreparedStatement:
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, id); // 参数通过set方法绑定,不能直接拼进sql字符串
ResultSet rs = ps.executeQuery();
如果你在用MyBatis,也要注意:#{}是预编译占位符,${}是字符串拼接。
比如WHERE id = #{id}安全,而ORDER BY ${column}存在注入风险。MyBatis中#{}是预编译,${}是拼接;
动态表名、排序字段不能预编译,必须用白名单校验。
存量业务如何排查拼接SQL?
在代码仓库里全局搜索这些特征,重点检查:
- 字符串拼接符号:PHP的
., Java的+,以及concat、String.format等。 - MyBatis XML里的
${}写法。 - 直接拼接在SQL后面的
$_GET、$_POST、request.getParameter()。
Linux下可以这样在当前目录代码中搜索:
grep -rn "SELECT.*WHERE.*+" --include="*.php" --include="*.java" .
grep -rn "\${}" --include="*.xml" .
搜到后逐一改成预编译写法,不要只看报告。存量的每一个拼接点,都可能是攻击入口。
避坑与验证:这样测才算真正防御
改完代码后,在测试环境用下面的字符串尝试注入:
1' OR '1'='1
1"; DROP TABLE users; --
如果查询结果与正常输入完全一致,或直接报参数类型错误,说明拼接已被拦截。
如果出现了额外数据或SQL报错中的语法片段,说明还有拼接点没改干净。
另一个常见的坑是:预编译只能处理“值”,不能处理表名、列名、排序字段。
如果业务确实需要动态表名或排序,最稳妥的做法是做成白名单,只允许传入固定的几个值,其他一律拒绝。
如果你正在处理MySQL注入漏洞防御,业务禁止拼接SQL语句强制预编译,建议先按本文步骤完整执行,再根据自己的环境做微调;
遇到异常时优先回看避坑和高频问题部分。