Skip to content

查询用了参数绑定,排序和搜索仍然可能出错 ​

把搜索条件换成 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 文档明确规定了这个范围。

排序选项外部输入有限映射price ASC / id DESC查询结构未知选项拒绝查表选片段条件值请求 / 存储数据参数绑定一个参数一个值执行查询运算符语义仍生效绑定传值结构LIKE 模式与 IN 列表的正确性,需要独立检查
图 1 · 输入的结构选项先映射到程序定义的片段,普通数据值进入绑定参数;保存过的数据再次使用时仍走值的路径

图中两条路径最终汇入同一条查询:程序生成有限的结构,驱动绑定数据值。即使输入先存进数据库、之后再被读出,它也应沿着值的路径使用;数据库保存过一遍不会赋予它成为 SQL 片段的资格。

实测中,绑定 ORDER BY ? 的值 price 没有报错,返回 ID 顺序为 [1, 2, 3, 4, 5],而按价格排序应为 [4, 1, 5, 3, 2]。这里绑定的是字符串常量,不是列引用;观察到的顺序也不构成结果顺序保证。ORDER BY price ? 绑定 DESC 则抛出 SQLSyntaxErrorException。别的数据库可能对这些写法直接报错,不能把这个结果推广为通用 SQL 行为。

防御代码可以把外部选项映射到固定片段。下面省略连接的创建与结果读取,sortKey 和 owner 是输入值:

java
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_ba_b tester 与 axb adapter
转义后的 a_b只返回 a_b tester

这些结果没有改变 SQL 结构,但可能违背「按用户输入的字面文本搜索」这个接口契约。允许用户写模式与提供字面搜索,是两个需要明确选择的功能。

字面搜索可以显式规定转义字符,避免依赖会话的反斜杠模式。以下是依据 MySQL 8.4 文档编写的机制片段,归档实验使用的是反斜杠转义;此处 ! 写法未计作实验结果:

java
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 或脚本时,需要按输出上下文分别处理。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权