嵌套子查询本身不引入SQL注入风险,真正危险的是在子查询中拼接用户输入;MyBatis的${}因字符串替换特性,用于表名、字段名或IN条件时可致任意SQL注入;#{}仅适用于单值绑定,动态结构须用<foreach>或白名单校验。

嵌套子查询本身不引入SQL注入风险,真正危险的是在子查询中拼接用户输入——尤其是用 ${} 或字符串拼接方式动态生成子查询条件时。
为什么 MyBatis 的 ${} 在子查询里特别危险
MyBatis 中 ${} 是字符串替换,不经过预编译;一旦用于子查询的表名、字段名或 WHERE 条件片段,攻击者就能注入任意 SQL。
- 比如:
WHERE status IN (${statusList}),若statusList = "1, 2, (SELECT password FROM users LIMIT 1)",就可能触发联合查询泄露数据 -
FROM ${tableName}或ORDER BY ${sortField}出现在子查询的FROM或SELECT子句中,会直接导致表名/字段名可控 - 即使外层用了
#{},只要子查询内部用了${},整个链路就失守
用 #{} 替代 ${} 的实际限制与绕过场景
#{} 只能安全绑定单个参数值(如字符串、数字),不能用于动态列名、表名、操作符或 IN 列表 —— 这些地方开发者常误以为“加个 #{} 就安全了”,结果仍留后门。
- 错误写法:
WHERE category_id IN (#{categoryIds})→ MyBatis 会把整个集合转成字符串,如'[1,2,3]',导致语法错误或逻辑失效 - 正确做法:用
<foreach></foreach>配合#{}逐项绑定:WHERE category_id IN <foreach item="id" collection="categoryIds" open="(" separator="," close=")">#{id}</foreach> - 若必须动态表名(极少数分表场景),应严格白名单校验:
if (!allowedTables.contains(tableName)) throw new IllegalArgumentException();,再用${}
嵌套子查询性能差 + 注入风险双叠加时怎么破
当业务要求“查出每个用户的最新订单,且该订单含指定类别的商品”这类逻辑,容易写出多层相关子查询 + ${} 拼接的组合拳——既慢又危险。
- 优先改写为 JOIN:把子查询逻辑下沉到
JOIN ... ON或LEFT JOIN LATERAL(PostgreSQL)/APPLY(SQL Server) - 避免在子查询中重复引用用户输入:例如主查询传入
#{userId},子查询就不要再从 request 或 session 里二次取值并拼接 - 对高频嵌套查询做物化:把中间结果存入临时表或带索引的冗余字段,用空间换安全和性能
- MyBatis 中可封装为
<sql>片段复用,确保所有引用都走#{}绑定,杜绝散落在各处的${}
最易被忽略的一点:测试时用固定值(如 status = 'shipped')看不出问题,但上线后用户输入含单引号、括号或分号时,子查询里的字符串拼接立刻崩盘——别依赖“没报错”来判断安全。

















