在 Web 安全领域,SQL 注入始终是 OWASP Top 10 的常客。很多开发者已经养成了使用参数化查询的习惯,但在遇到 IN 查询时,却常常不自觉地退回到字符串拼接的老路。这篇文章就来聊聊 IN 查询中那些容易被忽视的注入风险,以及正确的参数化写法。
为什么 IN 查询容易出问题
普通的等值查询很容易参数化:
SELECT * FROM users WHERE id = ?
但 IN 查询的占位符数量是动态的,取决于数组长度。很多开发者不知道如何为可变数量的参数生成占位符,于是选择了最"省事"的做法——直接拼接:
$ids = implode(',', $_GET['ids']);
$sql = "SELECT * FROM users WHERE id IN ($ids)";
这段代码看起来简洁,却把用户输入直接送进了 SQL 语句。攻击者只需传入 1) UNION SELECT username,password FROM admin--,就能读取敏感数据;传入 1); DROP TABLE users--,甚至可能直接删库。
问题的根源在于:IN 后面的括号里,本质上是 SQL 语法结构的一部分,而不是数据。当你把用户输入拼进去时,用户输入就不再是"数据",而变成了"代码"。
常见的错误写法
错误一:直接拼接数组
$ids = implode(',', $ids);
$sql = "SELECT * FROM users WHERE id IN ($ids)";
即使对每个元素做了 intval,也不推荐这种习惯。一旦某个环节漏掉了类型转换,或者字段类型从整型变成了字符串,漏洞立刻出现。
错误二:手动加引号
$ids = "'" . implode("','", $ids) . "'";
$sql = "SELECT * FROM users WHERE name IN ($ids)";
如果数组元素本身包含单引号,比如 O'Reilly,拼接就会破坏 SQL 结构,既可能报错,也可能被利用。
错误三:用 addslashes 或 mysql_real_escape_string
这两个函数在特定字符集下存在绕过风险,而且它们只处理字符串,对数字型注入无能为力。转义永远只是权宜之计,参数化才是根本解法。
正确的参数化写法
核心思路是:根据数组长度动态生成占位符,参数仍然通过绑定传入。
PHP PDO
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
array_fill 生成与数组等长的 ?,execute 时按顺序绑定。用户输入从头到尾都只是参数,不参与 SQL 解析。
Python(psycopg2 / pymysql)
ids = [1, 2, 3]
placeholders = ','.join(['%s'] * len(ids))
cursor.execute(f"SELECT * FROM users WHERE id IN ({placeholders})", ids)
注意 %s 只是占位符,具体的驱动会负责转义和绑定,不要把它和 Python 的字符串格式化混淆。
Java(JDBC)
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE id IN (" + placeholders + ")");
for (int i = 0; i < ids.size(); i++) {
ps.setInt(i + 1, ids.get(i));
}
Node.js(mysql2)
const placeholders = ids.map(() => '?').join(',');
const [rows] = await conn.execute(
`SELECT * FROM users WHERE id IN (${placeholders})`, ids);
这里拼接的只是 ? 字符,不含任何用户数据,因此是安全的。
几个容易踩的坑
空数组。如果数组为空,IN () 是非法语法。需要提前判断,直接返回空结果或跳过查询。
超长数组。MySQL 对预处理语句的参数数量有上限(默认 65535),超大数组需要分批查询。
类型不一致。如果字段是字符串而绑定了整数,某些数据库会隐式转换,可能导致索引失效。绑定前确认类型匹配。
占位符与参数顺序。如果 SQL 中还有其他参数,要保证绑定顺序与占位符出现顺序一致,否则会张冠李戴。
与其他安全话题的关联
IN 查询的注入只是 SQL 注入的一个缩影。要系统防御,还需要配合其他措施:数据库账号最小权限、关闭详细错误回显、对输入做白名单校验。如果你在写前端,别忘了 XSS 的常见方式和前端如何防御同样重要——注入类漏洞往往在前后端之间互相配合。运维层面,参考 Linux 服务器安全加固清单,把 Web 服务、数据库、系统账户的权限收紧,能大幅降低漏洞被利用后的影响面。
小结
IN 查询的注入风险,本质上是"动态 SQL 结构"与"用户数据"混在一起造成的。解决办法并不复杂:用占位符拼结构,用绑定传数据。无论用哪种语言、哪种数据库驱动,这个原则都适用。养成习惯后,你会发现参数化写法并不比拼接麻烦多少,但它挡住的是可能让整个系统沦陷的风险。
未经允许不得转载:任鹏个人博客 » IN 查询中的 SQL 注入风险与参数化写法


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