in(?)不能绑定数组,因所有主流驱动均只将单个占位符映射为一个标量值,传入数组会被转为字符串或报错;必须动态生成等长占位符序列并逐个绑定,同时严格校验元素类型、长度与字符集,并处理空数组及超长列表分批。

不能用 WHERE id IN (?)) 直接绑定数组,数据库驱动根本不支持这种展开;必须动态生成与数组长度一致的占位符序列,并逐个绑定值。
为什么 IN (?) 绑定数组会失败
几乎所有主流驱动(PDO、psycopg2、PyMySQL、YDB SDK)都只把单个占位符映射为一个标量值。传入 [1, 2, 3] 时,? 或 %s 不会自动“炸开”成三个参数,而是被当作整体转成字符串 '[1, 2, 3]' 或直接报错 not all arguments converted。PostgreSQL 的 ANY($1) 是特例,但要求输入是服务端构造的 array 类型,不能信任原始 JSON 数组。
正确做法:动态生成占位符 + 批量绑定
核心步骤就两步:先算长度,再拼占位符串,最后绑定整个数组。
-
psycopg2示例:placeholders = ','.join(['%s'] * len(ids)); cursor.execute(f"SELECT * FROM users WHERE id IN ({placeholders})", ids) -
PyMySQL同理:cursor.execute("SELECT * FROM users WHERE id IN ({})".format(','.join(['%s'] * len(ids))), ids) -
PDO(MySQLi 不支持):$placeholders = str_repeat('?,', count($ids) - 1) . '?'; $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)"); $stmt->execute($ids); - 数组为空时必须提前拦截,否则
IN ()语法错误;建议统一抛InvalidArgumentException或返回空结果集
字符串类型数组要额外做白名单校验
数字数组可用 intval 或 int() 强转,但字符串字段(如 name IN (?, ?, ?))不能只靠类型转换——攻击者可传 "admin'; DROP TABLE users--" 这类 payload。必须加一层过滤:
- 拒绝空字符串、超长项(如 >64 字节)、控制字符(
\x00-\x1f) - 限定允许字符集,例如仅字母、数字、下划线:
preg_match('/^[a-z0-9_]{1,64}$/', $name)(PHP)或re.match(r'^[a-z0-9_]{1,64}$', name)(Python) - 禁止任何带引号、分号、注释符的输入;宁可拒掉合法但边缘的输入,也不要冒险
YDB 和 PostgreSQL 的 ANY 用法不是银弹
ANY($1) 看似简洁,但有硬性前提:
- PostgreSQL 要求传入的是
ARRAY[1,2,3]或从string_to_array()构造的数组,不能直接把用户 JSON 数组喂进去 - YDB 要求参数类型声明为
list<uint64></uint64>或list<string></string>,且需在 prepare 阶段明确指定,运行时不能动态改 schema - 二者都不解决输入校验问题——若未过滤,
ANY(ARRAY['a','b'); DROP TABLE x--'])依然能注入
最易被忽略的一点:占位符数量不能超过数据库单次查询的参数上限(如 PostgreSQL 默认 65535,MySQL 约 65532),大数组要分批处理;另外,所有字段名、表名、排序字段都必须走白名单,绝不能因为用了参数化就放松对 SQL 结构的校验。










