in子句是sql注入高危区,因其无法用单个占位符绑定多个值,迫使开发者字符串拼接;java preparedstatement不支持动态参数展开,错误拼接或过滤绕过易致漏洞;安全方案须为每个值分配独立占位符并校验数量与类型。

IN子句为什么是SQL注入高危区
因为 IN 后面无法直接用单个占位符 ? 代替多个值,开发者被迫退回到字符串拼接——这是绝大多数 IN 相关漏洞的起点。
Java PreparedStatement 无法直接绑定 IN 列表
你写 SELECT * FROM users WHERE id IN (?),然后调用 setInt(1, 123),结果会报错:java.sql.SQLException: Parameter index out of range。PreparedStatement 的占位符只支持单值绑定,IN 需要动态数量的参数,框架不自动展开。
常见错误做法:
- 拼接字符串:
"WHERE id IN ('" + String.join("','", ids) + "')"—— 一旦ids里有' OR 1=1 --,立刻沦陷 - 用
String.format或StringBuilder拼IN列表 —— 和手写 SQL 没区别,只是换了个写法 - 误信“我过滤了单引号”就安全 —— 攻击者改用
%00编码、十六进制0x27、或绕过过滤的 Unicode 变体
真正安全的 IN 参数化方案
必须让每个值都走独立占位符,且数量可控。不要试图“通用化”,按实际需查的 ID 数量生成对应数量的 ?。
正确示例(Java):
String ids = List.of(1, 5, 9); // 来自可信来源或已校验的整型列表
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
String sql = "SELECT * FROM users WHERE id IN (" + placeholders + ")";
PreparedStatement stmt = conn.prepareStatement(sql);
for (int i = 0; i <p>关键点:</p>
-
ids必须是强类型集合(如List<integer></integer>),不能是List<string></string>再手动转——防止传入"1 OR 1=1" - 最大长度要有硬限制(比如最多 100 个 ID),防 DoS 和语法超长报错
- 若 ID 来自前端,必须先做类型校验:
ids.stream().allMatch(i -> i > 0 && i
MyBatis 中 $ 与 # 的生死线
在 MyBatis 的 XML 映射里:IN 场景下,${ids} 是炸弹,#{ids} 默认不生效——但 MyBatis 提供了安全解法。
正确写法(使用 <foreach></foreach>):
<select id="selectUsersByIds" resulttype="User">
SELECT * FROM users WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach></select>
注意:
-
collection必须是 List/Array,不能是逗号分隔的字符串 -
#{id}保证每个值单独参数化,不是字符串替换 - 如果传入的是字符串数组(如
["1", "2", "3"]),必须确保后端已转为整型,否则#{id}仍会当字符串处理,可能触发隐式类型转换漏洞
最常被忽略的一点:IN 列表长度突增时,不仅可能触发 SQL 长度限制或性能陡降,还可能暴露未设上限的业务逻辑——攻击者会故意传几千个 ID 来试探边界,再结合其他漏洞扩大战果。











