批量插入必须严格分离sql骨架与数据,禁用字符串拼接values列表;安全方案仅限orm原生批量方法或手写时确保骨架静态、参数绑定完整。

批量插入时拼接VALUES列表等于主动交出数据库控制权
只要用implode()、join()或+把用户传来的数组塞进INSERT INTO ... VALUES (...),就已触发高危漏洞。攻击者传["1", "2", "3', (SELECT @@version), '4"],最终执行的SQL会变成VALUES (1), (2), (3', (SELECT @@version), '4)——子查询直接被执行,版本信息泄露只是第一步。
为什么IN子句的防御方案不能直接套用到INSERT
INSERT不像WHERE IN有现成的数组绑定接口(如.in_()),开发者容易误以为“参数化占位符够用了”,却忽略VALUES必须与参数数量严格一一对应:
- 传入
[1, 2, 3],需生成(?), (?, ?), (?, ?)结构,而非单个? - ORM如Django的
bulk_create()或SQLAlchemy的insert().values()才真正隔离数据;手写executemany("INSERT ...", data)仍需确保data是二维序列,不能是字符串列表 - MySQL的
LOAD DATA INFILE或PostgreSQL的COPY虽快,但文件路径若含用户输入,仍是拼接——白名单校验路径前缀不可跳过
常见错误:看似安全的“动态字段名”和“批量值混合拼接”
以下写法全部高危,且不易被静态扫描发现:
-
f"INSERT INTO {table_name} ({', '.join(fields)}) VALUES {placeholders}"——表名、字段名未走白名单,placeholders即使用了?也救不了结构部分 -
cursor.executemany("INSERT INTO logs (msg) VALUES (%s)", [(f"User {user_id} logged in")])——日志内容拼进SQL字符串,不是绑定参数 -
db.execute(text(f"INSERT INTO {tbl} VALUES :vals"), {"vals": values})——:vals在text()字符串外,数据库不识别,实际仍是字符串拼接
真正安全的批量插入只有两条路
要么交给ORM的原生批量方法,要么严格控制SQL骨架+数据分离:
- Python SQLAlchemy:
stmt = insert(User).values([{"name": "a"}, {"name": "b"}]); conn.execute(stmt),values()接收字典列表,字段名自动映射,无需拼接 - Node.js pg:
client.query("INSERT INTO users(name) VALUES $1, $2", ["a", "b"])仅适用于固定列数;列数动态时必须用pg-format库生成占位符串,再传参,不能用模板字符串 - PHP PDO:
$placeholders = str_repeat('(?, ?, ?),', count($rows) - 1) . '(?, ?, ?)'; $stmt = $pdo->prepare("INSERT INTO t(a,b,c) VALUES $placeholders"); $stmt->execute(array_merge(...$rows));——注意$placeholders是纯字符串生成,$rows才是绑定数据
最易被忽略的是:批量操作的性能优化常让人铤而走险去“预生成SQL字符串”,但只要骨架里混入任何用户可控内容,参数化就彻底失效。安全和速度不是二选一,而是必须用正确工具达成两者兼得。










