批量插入是业务开发中非常常见的操作,比如导入 Excel 数据、批量写入日志、同步第三方接口返回的记录等。很多开发者认为批量插入只是“循环执行 INSERT”,因此在安全上放松了警惕。事实上,批量插入场景下的 SQL 注入风险并不比单条插入低,甚至因为拼接逻辑更复杂、参数更多,更容易出现漏洞。本文将从批量插入的常见写法出发,分析其中的注入风险,并给出可落地的防御方案。
一、批量插入的常见写法与风险
1. 循环拼接 SQL
最危险的写法是直接在循环中拼接 SQL 字符串:
foreach ($users as $user) {
$sql = "INSERT INTO users (name, email) VALUES ('" . $user['name'] . "', '" . $user['email'] . "')";
mysqli_query($conn, $sql);
}
如果 $user['name'] 来自用户输入,攻击者可以构造 name' , (SELECT ...))-- 之类的 payload,实现注入。即使每个字段都调用了 addslashes,也可能因为字符集问题被绕过。
2. 一次性拼接多组 VALUES
为了减少数据库交互次数,很多项目会拼接成一条 SQL:
$values = [];
foreach ($users as $user) {
$values[] = "('" . $user['name'] . "', '" . $user['email'] . "')";
}
$sql = "INSERT INTO users (name, email) VALUES " . implode(',', $values);
这种写法性能更好,但风险同样集中:只要有一个字段未正确转义,整条 SQL 都可能被注入。而且拼接后的 SQL 很长,出错后难以定位。
3. 使用预处理但只预处理部分字段
有些代码虽然使用了 PDO 预处理,但为了“灵活”,把表名、字段名或排序方向直接拼进 SQL:
$sql = "INSERT INTO {$table} ({$fields}) VALUES (?, ?)";
如果 $table 或 $fields 来自用户可控参数,注入依然存在。预处理只能保护“值”,不能保护“标识符”。
二、批量插入防御的核心原则
批量插入的防御并不复杂,关键是坚持三条原则:
- 值必须参数化:所有用户输入的值都通过占位符绑定,不拼接进 SQL。
- 标识符必须白名单:表名、字段名、排序方向等不能参数化的部分,必须用严格白名单校验。
- 批量操作要控制边界:限制单次插入条数、字段数量,避免超长 SQL 和资源耗尽。
三、推荐写法:预处理 + 批量绑定
1. PDO 批量插入
以 PDO 为例,推荐使用命名占位符或问号占位符,并利用 execute() 传入二维数组:
$sql = "INSERT INTO users (name, email) VALUES (?, ?)";
$stmt = $pdo->prepare($sql);
foreach ($users as $user) {
$stmt->execute([$user['name'], $user['email']]);
}
如果希望减少网络往返,可以开启事务:
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
foreach ($users as $user) {
$stmt->execute([$user['name'], $user['email']]);
}
$pdo->commit();
这种方式下,每个值都通过参数绑定,SQL 结构固定,注入风险基本消除。
2. 多组 VALUES 的参数化写法
如果确实需要一次插入多行,可以动态生成占位符,但值仍然绑定:
$placeholders = [];
$values = [];
foreach ($users as $user) {
$placeholders[] = "(?, ?)";
$values[] = $user['name'];
$values[] = $user['email'];
}
$sql = "INSERT INTO users (name, email) VALUES " . implode(',', $placeholders);
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
这里拼接的只是 (?, ?) 这种固定结构,不包含任何用户数据,因此是安全的。注意要限制 $users 的数量,避免 SQL 过长。
3. 标识符白名单校验
如果表名或字段名需要动态决定,必须使用白名单:
$allowedTables = ['users', 'orders', 'logs'];
if (!in_array($table, $allowedTables, true)) {
throw new InvalidArgumentException('Invalid table');
}
$allowedFields = ['name', 'email', 'created_at'];
$fields = array_intersect($fields, $allowedFields);
if (empty($fields)) {
throw new InvalidArgumentException('Invalid fields');
}
只有白名单内的标识符才允许拼接到 SQL 中,其他一律拒绝。
四、其他注意事项
1. 不要依赖 addslashes 或 mysql_real_escape_string
这两个函数在特定字符集下可能被绕过,而且它们只处理字符串,不处理数字、标识符。批量插入中字段类型多样,依赖转义函数容易遗漏。
2. 注意字段类型转换
对于整数、布尔值等非字符串字段,建议显式转换:
$age = (int)$user['age'];
$isActive = (int)(bool)$user['is_active'];
这样即使参数绑定失效,也不会因为类型问题引入注入。
3. 限制批量大小
单次插入建议不超过 500 到 1000 条,具体根据字段数量和数据库配置调整。过大的批量会增加 SQL 解析时间、锁持有时间,也可能触发 max_allowed_packet 限制。
4. 记录与监控
对批量插入操作记录日志,包括来源 IP、用户 ID、插入条数、耗时等。一旦出现异常批量写入,可以快速定位。
五、总结
批量插入场景下的 SQL 注入防御,核心不是“批量”,而是“参数化”。无论是一次插入一条还是一次插入一千条,只要用户数据进入 SQL 的方式是拼接,就存在注入风险。推荐做法是:用预处理绑定所有值,用白名单校验所有标识符,用事务和批量大小控制性能与安全边界。对于历史项目中已经存在的拼接写法,应优先排查批量导入、批量同步、后台管理等高危入口,逐步替换为参数化写法。安全不是一次性的工作,而是持续的习惯。
未经允许不得转载:任鹏个人博客 » 批量插入场景下的 SQL 注入防御写法


朋友圈点赞图在线生成源码