LIMIT后拼接字符串等于放弃防御底线,因数据库将整条拼接SQL当语法结构解析,不区分代码与数据;攻击者可利用多语句、类型混淆(如0x14、1e2)、注释符等绕过int强转,且MySQL默认不启用服务端预编译,导致参数化失效。

为什么 LIMIT 后拼接字符串等于放弃防御底线
因为数据库在解析 SQL 时,LIMIT 后的内容属于语法结构,不是数据值——它必须在执行前就确定字面量。一旦你写 "SELECT * FROM users LIMIT " + limitParam,无论 limitParam 是 "10" 还是 "10; DROP TABLE logs--",整条字符串都会被当作原始 SQL 提交给数据库解析。驱动不会帮你“识别这是数字就截断后面”,也不会自动过滤分号或注释符。
常见错误现象包括:
- MySQL 配置了
allowMultiQueries=true(某些旧版 Spring Boot 默认开启),传入limit=10; DROP TABLE users真的会删表 - 传
limit=0x14或limit=1e2,部分 ORM(如老版本 MyBatis)未做类型强校验,直接透传给 MySQL,导致预编译失效 -
limit=-1在某些 MySQL 版本中被静默转为0,结果查出全部数据,等同于没限制
int 强转根本拦不住注入,只是制造安全错觉
写 int(limitParam) 或 (int)$limit 看似保险,但只要上游没做输入清洗,攻击者就能绕过:PHP 的 (int)"10; DROP TABLE--" 得到 10,Go 的 strconv.Atoi("10 --") 也返回 10, nil。更危险的是,很多框架在转型失败后 fallback 到默认值(比如 0 或 10),而代码又没检查范围,导致 LIMIT 0 或 LIMIT 999999999999 这类失控查询。
真正该做的不是“转成 int”,而是从源头拒绝非法字符串:
- 用正则
^[0-9]+$严格匹配,不允许空格、负号、小数点、前导零、科学计数法 - 强转后立刻校验范围:
limit控制在1–1000,offset不得超过100000 - 任一环节失败,HTTP 返回
400 Bad Request,不设默认值、不重试、不打日志暴露细节
ORDER BY + LIMIT 组合才是高危连招
LIMIT 本身不能参数化,但很多人忽略了它常和用户可控的 ORDER BY 字段联动。比如后端拼接了 "ORDER BY " + sortField + " LIMIT ?",即使 LIMIT 参数走 setInt(),sortField 若没白名单校验,攻击者就能传 id, (SELECT password FROM users LIMIT 1),直接拖库。
典型错误场景:
- 前端传
?sort=id ASC, (SELECT @@version),后端只校验page和size类型,放行sort - MyBatis XML 里写
ORDER BY ${sortField},哪怕LIMIT #{size}是安全的,整个语句仍高危 - PageHelper 等插件在动态 SQL fallback 时,悄悄把
offset改成字符串拼接,#{offset}形同虚设
MySQL 的 useServerPrepStmts=true 不是默认开启的
很多人以为用了 PreparedStatement 就万事大吉,但 MySQL JDBC 驱动默认关闭服务端预编译(useServerPrepStmts=false)。这意味着即使你写 "LIMIT ?",驱动也会在客户端把问号替换成字面量再发给服务器——本质上还是字符串拼接。必须显式加连接参数:jdbc:mysql://host/db?useServerPrepStmts=true&cachePrepStmts=true。
PostgreSQL 和 SQL Server 没这个问题,LIMIT $1 或 TOP @size 天然支持服务端参数化。但前提是:你没在代码里自己拼 "LIMIT " + size。
最容易被忽略的一点:校验必须在拼接进 SQL 之前完成,且所有涉及分页的变量(limit、offset、sortField、direction)都要走独立、严格的白名单或正则校验——少一个,整条链路就崩。

















