SQL注入是数据库安全领域常见的风险,其中IN子句的注入问题尤为突出。很多开发者为了动态拼接查询条件,会直接将用户输入的参数拼接到IN子句的括号中,这种做法会让攻击者有机会注入恶意SQL语句,造成数据泄露、数据篡改等严重后果。

IN子句注入的成因
传统的IN子句动态拼接写法通常是这样的:假设我们需要查询id在指定列表中的用户,前端传入的id列表是1,2,3,如果不做处理直接拼接SQL,最终生成的语句可能是SELECT * FROM user WHERE id IN (1,2,3)。但如果攻击者传入的参数是1) UNION SELECT password FROM admin --,拼接后的SQL就会变成SELECT * FROM user WHERE id IN (1) UNION SELECT password FROM admin --),--是SQL的注释符号,后面的内容会被忽略,攻击者就能获取到管理员的密码信息。
参数化查询的核心思路
参数化查询的本质是将SQL语句的结构和参数值分离,数据库会对参数值做转义处理,避免参数值被当作SQL指令执行。对于IN子句来说,核心就是动态生成和参数数量匹配的占位符,再把参数值逐个传入,而不是拼接参数值到SQL字符串中。
不同数据库的占位符规则
不同数据库的参数占位符格式不同,动态构建时需要对应适配:
| 数据库类型 | 占位符格式 | 示例 |
|---|---|---|
| MySQL | ? | IN (?,?,?) |
| PostgreSQL | $n(n从1开始递增) | IN ($1,$2,$3) |
| SQL Server | @p1,@p2... | IN (@p1,@p2,@p3) |
| Oracle | :n(n从1开始递增) | IN (:1,:2,:3) |
多语言实现示例
Java实现(JDBC)
Java中使用JDBC时,可以根据参数列表的长度动态生成?占位符,再给PreparedStatement设置参数:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
public class SqlInExample {
public void queryUsers(Connection conn, List<Integer> idList) throws Exception {
// 动态生成占位符,数量和idList长度一致
String placeholders = String.join(",", java.util.Collections.nCopies(idList.size(), "?"));
String sql = "SELECT * FROM user WHERE id IN (" + placeholders + ")";
PreparedStatement ps = conn.prepareStatement(sql);
// 设置参数,索引从1开始
for (int i = 0; i < idList.size(); i++) {
ps.setInt(i + 1, idList.get(i));
}
ResultSet rs = ps.executeQuery();
// 处理结果集
while (rs.next()) {
System.out.println(rs.getString("username"));
}
}
}
Python实现(PyMySQL)
Python中使用PyMySQL操作MySQL时,占位符是%s,动态生成方式和Java类似:
import pymysql
def query_users(conn, id_list):
# 动态生成%s占位符,数量和id_list长度一致
placeholders = ",".join(["%s"] * len(id_list))
sql = f"SELECT * FROM user WHERE id IN ({placeholders})"
cursor = conn.cursor()
# 传入参数元组,注意参数要和占位符数量匹配
cursor.execute(sql, tuple(id_list))
result = cursor.fetchall()
return result
PHP实现(PDO)
PHP中使用PDO扩展时,占位符也是?,动态构建后绑定参数:
<?php
function queryUsers($pdo, $idList) {
// 动态生成?占位符
$placeholders = implode(",", array_fill(0, count($idList), "?"));
$sql = "SELECT * FROM user WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($sql);
// 绑定参数,索引从1开始
foreach ($idList as $index => $id) {
$stmt->bindValue($index + 1, $id, PDO::PARAM_INT);
}
$stmt->execute();
return $stmt->fetchAll();
}
?>
注意事项
- 如果IN子句的参数列表为空,需要特殊处理,避免生成IN ()这种非法SQL,比如可以在参数为空时直接返回空结果,或者拼接一个永远不成立的条件比如IN (NULL)。
- 参数类型要和数据库字段类型匹配,比如id是整型的话,设置参数时要转成整型,避免类型转换错误。
- 不要为了省事把整个IN子句作为参数传入,比如写成
IN (?)然后参数是"1,2,3",这种写法参数会被当作一个字符串,无法正确匹配多个值,也起不到参数化的作用。
通过上述动态构建参数化占位符的方式,既可以实现IN子句的动态查询需求,又能完全避免注入风险,是处理IN子句查询的最优方案。