LIMIT和OFFSET本身非漏洞,但用户输入未校验拼入SQL必致SQL注入;MySQL的LIMIT ?需useServerPrepStmts=true才真参数化;仅int转换无法防科学计数法、十六进制等绕过;安全须同时满足参数不拼接、驱动真预编译、数值白名单三条件。

LIMIT 和 OFFSET 本身不是“漏洞”,但只要用户输入未经校验就拼进 SQL 字符串,就必然触发 SQL 注入——哪怕它看起来只是个数字。
MySQL 的 LIMIT ? 看似支持参数化,但默认不生效
MySQL 5.7+ 确实允许写 LIMIT ? OFFSET ?,但这只是语法上“看起来能用”。真正起作用的前提是 JDBC 连接串必须显式启用服务端预编译:?useServerPrepStmts=true&cachePrepStmts=true。否则驱动会在客户端把 ? 直接替换成字符串,等价于手拼 SQL。
- 没加
useServerPrepStmts=true时,ps.setInt(1, 10)实际发出的是LIMIT 10,不是参数化执行 - 即使加了,若传入
1e2或0x14这类非十进制整数字符串,Integer.parseInt()会直接抛异常,而某些旧 ORM 会 fallback 到默认值或静默截断 - PostgreSQL 和 SQL Server 同理:
LIMIT $1或TOP @size都依赖底层驱动是否真走服务端准备语句
只做 int 强制转换根本挡不住真实攻击
把 request.getParameter("limit") 丢给 Integer.parseInt(),只能拦住 "10; DROP TABLE users--" 这种最粗糙的 payload。攻击者有大量绕过路径:
- 传
offset=1e9:部分驱动未校验科学计数法,转成 int 后溢出为负数或 0,导致OFFSET -1被 MySQL 静默转为 0,实际查全表 - 传
limit=0x14(即 20):十六进制字符串在某些框架里能绕过正则/^\d+$/,但parseInt("0x14", 16)仍得 20 - 传超大数
9999999999999999999:JavaLong.parseLong()溢出,Pythonint()虽无溢出但数据库可能拒绝或卡死 - 更危险的是联动注入:哪怕
OFFSET是 int,若ORDER BY ${sortField}没白名单,攻击者就能塞进id, (SELECT password FROM users LIMIT 1)
安全写法必须同时满足三个硬条件
缺一不可:参数不拼接、驱动真预编译、数值有白名单。任何环节松动,防护就归零。
- JDBC 层:SQL 模板写
"SELECT * FROM t ORDER BY id DESC LIMIT ? OFFSET ?";连接 URL 必须含?useServerPrepStmts=true;ps.setInt(1, size)前校验size > 0 && size ;ps.setInt(2, offset)前校验offset >= 0 && offset - MyBatis 层:XML 中只用
LIMIT #{size} OFFSET #{offset},禁用任何${size};确认 mapper 方法参数是Integer或基本类型,不是Map(后者可能被 MyBatis 动态解析为字符串拼接) - 校验必须返回 400:超出范围不能 fallback 到
offset=0或limit=10,否则攻击者能靠错误响应判断边界
ORDER BY 字段才是分页链路上最脆弱的一环
LIMIT 和 OFFSET 至少还能靠参数化 + 校验兜底,但 ORDER BY 字段根本无法参数化——数据库协议不允许 ORDER BY ?。这意味着所有动态排序都必须依赖白名单校验:
- 前端传
sort=id ASC,后端不能只校验ASC/DESC,必须拆解字段名并比对白名单:["id", "created_at", "status"] - 禁止用
ORDER BY ${sortField},哪怕你加了WHERE sortField REGEXP '^[a-z_]+$'也不行——正则拦不住id, (SELECT @@version) - 如果业务需要多字段排序(如
sort=id,created_at),白名单必须扩展到组合形式,或改用固定字段映射(sort=recent → ORDER BY created_at DESC, id DESC)
真正难的从来不是“怎么写 LIMIT”,而是确保从请求入口到 SQL 执行的每一步,都没有任何字符串拼接发生。一旦出现 "LIMIT " + size 或 ORDER BY ${field},后面所有校验和转换都是马奇诺防线。











