查询用了参数绑定,排序和搜索仍然可能出错
把搜索条件换成
PreparedStatement后,输入不再改变 SQL 的结构。但排序列绑成参数没有按价格排序,搜索a_b却查到了axb adapter,把三个 ID 拼成一个字符串也只查到了一行。
参数绑定解决的是值与 SQL 结构的分离。查询的列名、排序方向仍需要程序选择;值进入 LIKE 或类型转换后,还会受到运算符语义影响。检查查询代码时,需要分别确认结构来源、值的绑定方式和业务匹配规则。
本文依据文末归档实验的输出,环境为 MySQL 8.4.11、Connector/J 9.7.0、容器内 JDK 25.0.4.1。
一、先确定哪些位置允许输入
MySQL 的参数标记只能出现在允许数据值的位置,不能代替标识符或 SQL 关键字。比如用户名称可以绑定,ORDER BY 后面的列名和方向需要从程序定义的选项中选择。MySQL PREPARE 文档明确规定了这个范围。
图中两条路径最终汇入同一条查询:程序生成有限的结构,驱动绑定数据值。即使输入先存进数据库、之后再被读出,它也应沿着值的路径使用;数据库保存过一遍不会赋予它成为 SQL 片段的资格。
实测中,绑定 ORDER BY ? 的值 price 没有报错,返回 ID 顺序为 [1, 2, 3, 4, 5],而按价格排序应为 [4, 1, 5, 3, 2]。这里绑定的是字符串常量,不是列引用;观察到的顺序也不构成结果顺序保证。ORDER BY price ? 绑定 DESC 则抛出 SQLSyntaxErrorException。别的数据库可能对这些写法直接报错,不能把这个结果推广为通用 SQL 行为。
防御代码可以把外部选项映射到固定片段。下面省略连接的创建与结果读取,sortKey 和 owner 是输入值:
Map<String, String> sorts = Map.of(
"price-low", "price ASC, id ASC",
"newest", "id DESC"
);
String order = sorts.get(sortKey);
if (order == null) {
throw new IllegalArgumentException("unsupported sort");
}
String sql = "SELECT id FROM products WHERE owner = ? ORDER BY " + order;
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, owner);
// 执行查询并关闭 ResultSet。
}映射值必须由代码定义。对输入做「像列名」的正则检查,只能约束字符形态,不能决定用户是否有权选择某个字段;表名、投影列等结构位置也应采用有限映射。值仍然绑定,不拼回 SQL。
二、绑定后的模式仍然是模式
LIKE 把 % 解释为任意长度字符序列,把 _ 解释为单个字符。参数绑定不会改变这种规则。在归档实验中:
| 绑定的搜索值 | 查询结果 |
|---|---|
% | 5 行全部返回,其中只有 1 个名称真的带百分号 |
a_b | a_b tester 与 axb adapter |
转义后的 a_b | 只返回 a_b tester |
这些结果没有改变 SQL 结构,但可能违背「按用户输入的字面文本搜索」这个接口契约。允许用户写模式与提供字面搜索,是两个需要明确选择的功能。
字面搜索可以显式规定转义字符,避免依赖会话的反斜杠模式。以下是依据 MySQL 8.4 文档编写的机制片段,归档实验使用的是反斜杠转义;此处 ! 写法未计作实验结果:
String escaped = input.replace("!", "!!")
.replace("%", "!%")
.replace("_", "!_");
String pattern = "%" + escaped + "%";
String sql = "SELECT id FROM products WHERE name LIKE ? ESCAPE '!'";将 pattern 用 setString 绑定。转义字符自身要先处理;大小写、重音、尾部空格等匹配行为还受字符集和排序规则影响。若接口承诺精确相等,应另选合适的比较方式。LIKE 与 ESCAPE 文档说明了通配符和 SQL 模式的关系。
三、一个占位符只接收一个值
IN (?) 绑定字符串 "1,2,3" 不会展开成三个参数。实测在该 MySQL 环境下只返回 ID 1,这是隐式类型转换的结果;同一实验使用 IN (?, ?, ?) 逐个绑定,返回 ID 1、2、3。
程序可以按列表长度生成占位符,列表中的每个元素仍通过绑定传入。空列表要在业务层明确处理,不能生成 IN ();列表长度需要上限,避免把过大的请求变成解析和执行负担。不要为了列表支持开启多语句执行,也不要把逗号分隔字符串直接放进结构位置。
四、复核查询时覆盖整条路径
参数化与业务授权需要分别检查。查询即使结构固定,仍可能缺少租户过滤、读取超出权限的数据,或允许一个过大的查询范围。模型生成整条 SQL 时,结构本身也是外部输入,不能靠绑定几个参数完成验收;进一步见生成 SQL 的执行检查。
给每个动态查询建立小型验证集:合法排序与未知排序、字面 % 和 _、空列表与多个元素、数据读取后再次查询。测试既要确认查询结构不能被外部输入改变,也要确认结果满足接口契约。关闭多语句执行可以减少能力,但不能代替参数绑定或结构映射。
浏览器还有另一组解析边界:同一份数据进入文本、属性、URL 或脚本时,需要按输出上下文分别处理。
配套实验
- codesphere-labs/engineering/sql-injection-boundaries:MySQL 8.4.11、Connector/J 9.7.0(JDK 25.0.4.1),本地容器里的演示数据:值的位置拼接与绑定、排序列与方向能否绑定、白名单、
LIKE的通配符与转义、IN (?)与逐个占位符、多语句执行(验证记录)
参考资料