limit和offset分页存在性能与安全双重风险:大offset需扫描丢弃大量行导致慢查询,且仅靠int转型无法防sql注入,必须结合预编译参数、白名单校验及游标分页优化。

直接对 LIMIT 或 OFFSET 参数做强制转型(比如转成 int)不能彻底解决 SQL 注入,它只拦得住最表层的拼接型攻击,但挡不住绕过手段和深层逻辑漏洞。
为什么 int 强制转型不等于安全
很多人以为只要把前端传来的 page、size 用 int.Parse() 或 Integer.valueOf() 转一下就万事大吉——其实这只是防住了字符串里混进 SQL 片段(比如 "10; DROP TABLE users--"),但以下情况依然危险:
- 如果转型失败后 fallback 到默认值(如
0或1),而代码又没校验范围,可能触发LIMIT 0, 1000000这类资源耗尽式查询 - 若参数被用于动态构造排序字段(如
ORDER BY ${sortField}),哪怕sortField是字符串,也完全不受int转型保护 - 某些 ORM 或分页插件在底层仍会把数字参数拼进 SQL 字符串(例如旧版 MyBatis +
${}语法),转型毫无意义
真正起作用的防护点在哪儿
SQL 注入防御的核心不是“参数看起来像数字”,而是“参数内容绝不会进入 SQL 解析上下文”。关键动作包括:
- 所有用户输入(包括页码、每页条数、排序字段、搜索关键词)必须通过预编译参数(
?或@param)传入,不能出现在 SQL 字符串模板中 -
LIMIT和OFFSET在 MySQL 中支持参数化,但仅限于 MySQL 5.7+ 且驱动需开启useServerPrepStmts=true;PostgreSQL 的$1、SQL Server 的@size同理 - 如果用的是 MyBatis,必须用
#{}包裹数值参数,禁用${};MyBatis-Plus 的Page对象本身是安全的,但自定义 SQL 里若手写limit #{size},要确认底层驱动是否真走预编译 - 对
ORDER BY字段等无法参数化的部分,只能白名单校验:比如只允许"id"、"created_at"、"status"几个固定值
常见错误写法与修正对比
错误示例(Java + JDBC):
String sql = "SELECT * FROM orders ORDER BY created_at DESC LIMIT " + size + " OFFSET " + offset; // 即使 size 和 offset 是 int,这里仍是字符串拼接,不可信
正确写法(使用 PreparedStatement):
String sql = "SELECT * FROM orders ORDER BY created_at DESC LIMIT ? OFFSET ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, size); // size 必须先校验:> 0 && = 0 && <p>MyBatis XML 中的错误写法:</p><pre class="brush:php;toolbar:false;"><select id="list">
SELECT * FROM user WHERE name LIKE '%${name}%' LIMIT ${size} OFFSET ${offset}
</select>应改为:
<select id="list">
SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%') LIMIT #{size} OFFSET #{offset}
</select>Offset 大时的隐性风险比注入更值得警惕
当 OFFSET 达到几十万,即使参数是干净的 int,也会导致数据库扫描并丢弃大量行,拖慢整个实例。这时候真正的瓶颈不是安全,而是性能。游标分页(WHERE id > ? ORDER BY id LIMIT ?)能同时规避深度分页和注入风险——前提是主键或排序字段严格单调、无重复,且前端能可靠传递上一页末尾值。这点常被忽略,却直接影响系统稳定性。











