limit不能使用参数化查询,因其属于sql语法结构而非数据值,必须在解析阶段确定字面量;需用正则^\d+$严格校验limit/offset字符串,再强转并检查范围(如1–1000),任一失败即返回400且不设默认值。

直接拼接用户输入到 LIMIT 子句中,是典型的“看似可控、实则高危”的写法——数据库驱动不支持参数化 LIMIT,但很多人误以为“只是数字,没问题”,结果放行了 -1、0x7FFFFFFF、999999999999999999999 甚至带空格的 " 10 ",导致全表扫描或解析异常。
为什么 LIMIT 不能用 ? 或 $1 占位符
几乎所有主流驱动(psycopg2、pymysql、mysql2、pgx)在执行时会报错:syntax error at or near "$1" 或 Incorrect usage of parameterized query with LIMIT。因为 SQL 标准规定 LIMIT 和 OFFSET 的值属于“语法结构部分”,不是“数据值”,数据库引擎在解析阶段就需要确定其字面量,无法延迟绑定。
如何安全地校验并转换 limit/offset 参数
关键不是“转成整数”,而是“从字符串源头就拒绝一切非纯数字”。parseInt("10abc") 得到 10,int(" 10 ") 在 Python 中能成功但隐含空格风险,这些都会绕过业务逻辑限制。
- 先用正则严格匹配:
^[0-9]+$(不允许负号、小数点、空格、前导零、科学计数法) - 再强转:Python 用
int(),Go 用strconv.Atoi(),Node.js 用Number()+isFinite()+Number.isInteger() - 立即检查范围:
limit通常 1–1000,offset建议上限设为 100000(防深度翻页) - 任一环节失败,返回 HTTP 400,**不 fallback 到默认值**——默认值会掩盖非法输入,让攻击者持续试探
拼接进 SQL 前的最后一道防线
即使校验通过,拼接时也建议加一层防御性断言:
sql := "SELECT * FROM orders ORDER BY created_at DESC LIMIT " + strconv.Itoa(limit)
// ✅ 可选:运行时再确认
if limit 1000 {
http.Error(w, "invalid limit", http.StatusBadRequest)
return
}
注意:不要用 fmt.Sprintf("LIMIT %d", limit) 替代字符串拼接——如果 limit 是 int 类型且已校验,它和直接拼接等价;但如果变量类型没锁死(比如来自 map[string]interface{}),仍可能 panic。
OFFSET 同样危险,且更容易被忽略
OFFSET 比 LIMIT 更隐蔽:传入 offset=1e9 可能触发数据库内存溢出;offset=-1 在某些 MySQL 版本下被静默转为 0;而 offset=abc 若未校验,会导致 SQL 解析失败并暴露错误细节。所有涉及分页的偏移量,都必须走和 limit 完全一致的校验流程——包括正则、强转、范围检查。
真正麻烦的不是写校验逻辑,而是把所有分页入口(API 查询参数、管理后台表单、内部服务调用)都统一收口到同一套校验函数里;漏掉任意一个,就等于留了一扇没锁的后门。











