首先得说清楚一个事实:LIMIT 这玩意儿,本质上就是个“限流阀”,它管不了你的 SQL 语句有没有被篡改。攻击者要是存心使坏,用 UNION SELECT、INSERT、SELECT ... INTO OUTFILE 或者干脆来个多语句(比如 ; 后面直接接 DROP TABLE),你光靠一个 LIMIT 是拦不住的。真正能扛事的,只有参数化查询。LIMIT 的作用,充其量是在参数化查询这个安全框架下,作为辅助手段帮你控制一下数据量,防止“拖库”时把整个家底都搬走,把危害面尽可能地缩小。

为什么直接拼接 LIMIT 参数等于开门揖盗
很多新手会写出这样的代码:$limit = (int)$_GET['limit']; $sql = "SELECT * FROM users LIMIT $limit";。表面上看,已经做了个整型转换,但问题在于,一旦上游的校验不够严格,比如传了个 limit=100; DROP TABLE users,或者 PHP 的配置里还开着 register_globals 这种古董级机制,那多语句执行就可能被触发。更隐蔽的坑在于,整型转换并不拦截负数或者天文数字。比如 9223372036854775807 这个值,MySQL 会直接当成“给我返回全部数据”,LIMIT 的限制意义瞬间归零。
正确的做法,必须满足两个硬性条件:
- 所有用户输入,包括
LIMIT的值,都必须走bindValue()这类绑定操作,并且显式地指定类型为PDO::PARAM_INT。 - 在代码里,必须给
LIMIT的值设一个硬上限,比如最多 100 条。一旦超出,直接截断或者拒绝,绝不能依赖前端传过来的值。
不同数据库对 LIMIT 参数化的支持差异
MySQL 和 PostgreSQL 都支持直接绑定 LIMIT ?,但 SQLite 就会报错:near "+": syntax error,如果你写的是 LIMIT ? + 1。PostgreSQL 不接受负数 LIMIT,传 0 就返回空结果集;而 MySQL 允许负数,但它会默默地转为 0,行为上并不一致。这些细节如果不处理,上线后很容易在某个环境里突然崩掉,排查起来相当头疼。
安全写法的示例(PDO + MySQL):
SELECT id, name FROM users WHERE status = ? LIMIT ?
执行时,代码要这么写:
$stmt->bindValue(1, $status, PDO::PARAM_STR);
$stmt->bindValue(2, min((int)$limit, 100), PDO::PARAM_INT);
表名、字段名、排序方向不能参数化,白名单是唯一解
哪怕 LIMIT 绑定了,但如果代码里还这么写:"ORDER BY $sort_field $sort_dir",攻击者只要传个 sort_field=id; DROP TABLE logs,你就直接中招了。这类动态的 SQL 片段,没法用占位符,唯一的办法就是用白名单硬控:
- 定义一个白名单数组,比如
$allowed_tables = ['users', 'orders', 'products'],校验通过之后再拼接。 - 字段名也要有白名单,比如
$allowed_fields = ['id', 'name', 'created_at'],而且最好映射到真实的字段名,避免暴露内部的逻辑别名。 - 至于
ASC/DESC,必须从一个固定的字符串数组里取,绝不能直接拼接用户输入。
最容易忽略的一个点:很多开发者觉得“只要用了 ? 就安全了”,结果把表名、分页的偏移量(OFFSET)、GROUP BY 字段全当成变量拼接进去。这些地方根本没有占位符的语法,白名单校验只要漏了一项,整条查询的防线就形同虚设了。
