仅对 LIMIT 和 OFFSET 做 int 强制转换无法防御 SQL 注入,因数据库协议中 LIMIT 值属语法结构而非数据值,且存在多语句、类型混淆、驱动未启用服务端预编译、ORDER BY 字段注入等绕过路径。

仅对 LIMIT 和 OFFSET 参数做 int 强制转换,不能防 SQL 注入。它只拦得住最表层的字符串拼接,但挡不住多语句、类型混淆、驱动未启用预编译、或与 ORDER BY ${sortField} 联动等真实攻击路径。
为什么 LIMIT 后面的数字也能被注入
因为数据库协议本身不支持把 LIMIT 的值当作参数绑定——它属于语法结构,不是数据值。MySQL 5.7+ 虽然支持 LIMIT ? OFFSET ?,但前提是 JDBC 连接串必须显式开启 useServerPrepStmts=true;否则驱动会在客户端把 ? 拼成字符串,预编译完全失效。
- 攻击者传
10; DROP TABLE users--,若数据库连接允许多语句(allowMultiQueries=true),真会执行删表 - 传
1e2、0x14、-1或超大数如9999999999999999999,部分 ORM 或旧驱动未严格校验就透传,导致整数溢出或全表扫描 -
OFFSET常和用户可控的排序字段耦合,比如ORDER BY ${sortField} LIMIT ? OFFSET ?,即使OFFSET是 int,sortField若没白名单,就能注入子查询:id, (SELECT password FROM users LIMIT 1)
Java JDBC 中正确使用 PreparedStatement 绑定 LIMIT/OFFSET
必须同时满足三个条件:SQL 模板用 ? 占位、驱动启用服务端预编译、参数在 setInt() 前完成范围校验。
- JDBC URL 加上
?useServerPrepStmts=true&cachePrepStmts=true,否则LIMIT ?会被客户端拼接 - SQL 写成
"SELECT * FROM orders WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?",不能含任何字符串拼接 -
ps.setInt(2, size)前必须校验:size > 0 && size <= 100;ps.setInt(3, offset)前校验:offset >= 0 && offset <= 1000000 - 校验失败直接返回 400,不 fallback 到默认值(比如
offset=0)——fallback 会掩盖非法输入,给攻击者试探空间
MyBatis 中禁用 ${},只用 #{} 处理分页参数
${} 是字符串替换,无论里面是不是数字都会拼进 SQL;#{} 才走预编译。但要注意:MyBatis 的 #{} 对 LIMIT 的支持依赖底层驱动是否启用服务端预编译,不是写对了就一定安全。
- XML 映射中必须写
LIMIT #{size} OFFSET #{offset},绝不能出现LIMIT ${size} - Mapper 方法参数必须是基本类型或包装类(如
Integer),不能是Map<String, Object>或泛型对象——后者可能触发 MyBatis 回退到字符串拼接 - MyBatis-Plus 的
Page<T>对象本身安全,但自定义 SQL 里如果手写了LIMIT #{size},仍要确认运行时是否真调用了PreparedStatement.setInt() -
ORDER BY字段无法参数化,只能硬编码白名单:if (!Arrays.asList("id", "created_at", "status").contains(sortField)) throw new IllegalArgumentException();
所有语言都必须先正则校验再强转,不能只靠 parseInt/int()
很多语言的 parseInt("10abc") 返回 10,int(" 10 ") 在 Python 中成功但隐含空格风险——这些都会绕过“看起来是数字”的表层防护。
- 原始字符串必须先用正则
^[0-9]+$校验:不允许负号、小数点、空格、前导零、科学计数法 - 再用语言原生强转函数:
Integer.parseInt()(Java)、int()(Python)、strconv.Atoi()(Go)、Number()+isFinite()+Number.isInteger()(Node.js) - 立即检查范围:
limit通常限制在 1–1000,offset上限建议设为 100000(防深度翻页 DoS) - 任一环节失败,HTTP 状态码 400 返回,不设默认值,不记录日志后继续执行
真正难的不是写对 ps.setInt(1, size),而是确保从请求解析、参数提取、类型转换、范围校验、SQL 构造、驱动执行,整条链路上每个用户可控点(分页、排序、搜索、过滤)都遵循同一套校验和传参规则——漏掉任意一环,防护就形同虚设。

















