直接拼接用户输入数组到IN子句100%导致SQL注入;必须动态生成等量占位符并逐个绑定参数,同时严格校验数组元素类型与格式,禁止字符串拼接。

直接用 IN 拼接用户输入的数组,不加处理,100% 存在 SQL 注入风险。 常见错误是把 $_POST['ids'] 或 JSON 数组直接 implode 后塞进 SQL 里,比如 WHERE id IN (1,2,3) 看似安全,但一旦输入变成 1,2,3); DROP TABLE users; -- 就彻底失控。
为什么 IN 不能直接参数化?
绝大多数数据库驱动(如 PHP 的 PDO、Python 的 psycopg2)只支持单值占位符(? 或 $1),不支持一个占位符展开为多个值。你写 WHERE id IN (?),传入数组 [1,2,3],结果不是三个值,而是把整个数组当字符串转成 'Array' 或报错。
- MySQLi 不支持
IN (?)绑定数组 - PDO 默认模式下
execute([ [1,2,3] ])会被当作单个参数,不是展开 - PostgreSQL 的
ANY($1)是例外,但需配合array类型,且输入必须是服务端构造的数组,不能直接信任用户 JSON
安全做法:动态生成占位符 + 批量绑定
核心思路是:先确定数组长度,生成对应数量的 ?(或 $1, $2, $3),再逐个绑定。不拼接、不字符串替换、不信任任何原始输入。
- PHP + PDO 示例:
$ids = $_POST['ids'] ?? [];<br>// 强制转整型并过滤空值<br>$ids = array_filter(array_map('intval', $ids), 'is_int');<br>if (empty($ids)) {<br> throw new InvalidArgumentException('ID 列表不能为空');<br>}<br>$placeholders = str_repeat('?,', count($ids) - 1) . '?';<br>$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");<br>$stmt->execute($ids); - Node.js + pg 示例:
const ids = req.body.ids.map(x => parseInt(x, 10)).filter(Number.isSafeInteger);<br>const placeholders = ids.map((_, i) => `$${i + 1}`).join(',');<br>const res = await client.query(`SELECT * FROM users WHERE id IN (${placeholders})`, ids); - 绝对不要这样做:
"IN (" . implode(',', $ids) . ")"—— 即使$ids是数字数组,也不能排除中间件或代理注入字符串
字符串类型数组更要小心类型校验
如果 IN 用于字符串字段(如 name IN ('a','b')),光用 intval 不行,必须做白名单或长度/格式限制。
- 允许的字符集明确(如仅字母+下划线):
array_filter($names, fn($n) => preg_match('/^[a-z_]{1,32}$/', $n)) - 拒绝空字符串、超长项(如 >64 字节)、控制字符(
\x00-\x1f) - PostgreSQL 可用
ANY(ARRAY[$1,$2,$3]),但前提是$1等已被绑定为text类型,且驱动支持数组参数(如pg的client.query默认支持) - ORM 如 Laravel 的
whereIn()、Django 的__in内部就是走的这种动态占位符逻辑,但前提是别绕过 ORM 直接拼 SQL
最易被忽略的一点:前端传来的数组可能含非预期结构,比如 {"ids": ["1", "2", "3"]} 和 {"ids": "1,2,3"} 混用,后端没做 schema 校验就直接 explode,会把恶意字符串喂给 IN。类型检查和结构清洗必须在绑定前完成,不能依赖“看起来像数字”这种模糊判断。

















