直接拼接字符串构造IN列表100%触发SQL注入,因数据库将整段拼接内容视为字面量而非多个独立值,且类型错配会导致索引失效或报错;必须动态生成等量占位符并绑定,空数组需转为WHERE 1=0,超长列表须分批。

直接拼接字符串构造 IN 列表,100% 触发 SQL 注入,不是“可能”,而是“必然”。
为什么 IN 子句拼字符串等于开门揖盗
数据库不解析 IN 后面的字符串为多个值——它只把整段拼出来的内容当做一个字面量。比如你写 "SELECT * FROM user WHERE id IN ('" + ids.join("','") + "'),传入 ["1", "2', DROP TABLE users--"],最终执行的就是:
SELECT * FROM user WHERE id IN ('1', '2', DROP TABLE users--')注释符 -- 会吃掉后续条件,DROP TABLE 直接执行。这不是边缘 case,是所有拼接型写法的默认行为。
更隐蔽的问题:即使输入“干净”,类型错配也会让索引失效。整型字段查 '1','2'(带引号字符串),MySQL 可能放弃索引走全表扫描;PostgreSQL 直接报 operator does not exist: integer = text。
IN 的参数化不是“自动的”,必须手动对齐占位符数量
几乎所有驱动(psycopg2、mysql2、sqlite3、pg)都不支持一个 ? 或 $1 绑定数组。写成 WHERE id IN (?) 并传入 [1,5,9],结果要么报错 bind parameter 1 not usable as scalar,要么只取第一个值。
正确路径只有一条:根据数组长度,动态生成等量占位符:
-
Array(ids.length).fill('?').join(',')→'?, ?, ?' -
str_repeat('?,', count($ids)-1) . '?'(PHP) -
','.join(['%s'] * len(ids))(Python + psycopg2)
这个占位符字符串里不能含任何用户数据,只允许 ?、,、括号——安全边界就卡在这一步。
空数组和超长列表不处理,线上必炸
IN () 是语法错误,几乎所有数据库都拒绝执行。不拦截空数组,服务直接 500;不控制长度,可能触发:
- MySQL:
max_allowed_packet超限,连接中断 - PostgreSQL:单语句最多 65535 参数,超了报
too many parameters - SQLite / Drizzle:>999 项直接
throw(不是静默截断)
应对方式必须显式:
- 空数组 → 改写为
WHERE 1=0或跳过该条件 - 超长列表 → 主动分批(如每 500 项一批),不要依赖驱动兜底
ORM 的 whereIn 不是免检金牌
Knex、Django ORM、Drizzle、Prisma 确实封装了 whereIn,但它们底层仍是动态占位符——而边界逻辑几乎都不代劳:
- Django 遇到空列表抛
EmptyResultSet,不捕获就是 500 - Drizzle 的
inArray()在 SQLite 下 >999 项直接 throw,需手动切片 - MyBatis 的
<foreach>若误用${id}(非#{id}),照样拼接字符串 - SQLAlchemy 的
text()+format(),哪怕只拼一个IN,也立刻沦陷
最易被忽略的点:空数组处理逻辑,往往在压测或运营活动时突然暴露——因为平时测试很少覆盖零选项场景。

















