limit和offset必须在应用层强校验后拼接,而非依赖参数化或int转型;order by字段须白名单校验,否则即使limit安全,sql仍可能被注入。

LIMIT 和 OFFSET 不能靠参数化兜底,必须在应用层强校验后拼接
MySQL 5.7+、PostgreSQL 等虽支持 LIMIT ? OFFSET ? 语法,但实际是否走预编译取决于驱动配置(如 MySQL 需 useServerPrepStmts=true)和 ORM 实现。多数生产环境里,LIMIT 后的值最终仍是字符串拼接进 SQL 的——这意味着“转成 int 就安全”是错觉,真正防线在应用层校验与拼接控制。
为什么 int 强制转型拦不住注入
常见错误是只做 int(page) 或 Integer.valueOf(pageStr),然后直接拼进 SQL。这挡不住:
- 传入
10; DROP TABLE users--:某些 JDBC 驱动开启allowMultiQueries=true时真会执行多语句 - 传入
1e2、0x14、+10:部分语言的类型转换会接受这些,但数据库解析时可能触发异常或绕过校验 - fallback 到默认值:比如
limit = limit ?? 10,当输入为空或非法时自动补 10,结果让攻击者用?limit=绕过校验逻辑 - 校验和拼接不在同一路径:前端传
limit=abc,后端先转int失败 → fallback → 再拼进 SQL,整个链路已失守
必须做的三步校验(缺一不可)
所有用户输入的 limit 和 offset 必须按顺序完成以下动作,任一失败立即返回 400:
- 原始字符串正则校验:
^[0-9]+$(禁止负号、小数点、空格、科学计数法符号) - 转整型后范围检查:
limit限定在1–100,offset不得超10000(防深分页 DoS) - 拼接前再次确认变量类型是
int(不是interface{}或string),再用"LIMIT %d OFFSET %d"格式化
示例(Go):
limitStr := r.URL.Query().Get("limit")
if !regexp.MustCompile(`^[0-9]+$`).MatchString(limitStr) {
http.Error(w, "invalid limit", http.StatusBadRequest)
return
}
limit, err := strconv.Atoi(limitStr)
if err != nil || limit 100 {
http.Error(w, "limit out of range", http.StatusBadRequest)
return
}
// ✅ 此时可安全拼接
sql := fmt.Sprintf("SELECT * FROM orders ORDER BY id DESC LIMIT %d OFFSET %d", limit, offset)
ORDER BY 字段才是最危险的盲点
LIMIT 和 OFFSET 至少还能靠强校验堵住,但和它们常绑定的 ORDER BY 字段几乎从不参数化——因为数据库不支持 ORDER BY ?。一旦允许用户传 sort=id ASC, (SELECT password FROM users),哪怕 limit 安全,整条查询也沦陷。
- 排序字段必须白名单校验,例如只允许
"id"、"created_at"、"status" - 禁止任何形式的字符串拼接,包括
"ORDER BY " + sortField,哪怕sortField已转为string - MyBatis 中禁用
${sortField},只用#{sortField}且配合@Param显式传入白名单值 - GORM 若用
Order("created_at DESC"),确保"created_at"是硬编码,而非来自r.URL.Query().Get("sort")
拼接 LIMIT 和 OFFSET 本身不优雅,但它是当前最可靠的做法;真正容易被忽略的是——ORDER BY 字段没白名单,LIMIT 再严也没用。











