子查询参数化必须独立处理,否则存在sql注入风险;常见错误是仅对主查询使用preparedstatement而拼接子查询,或在mybatis中误用${}导致绕过#{}防护;正确做法是将子查询也参数化或使用预编译集合参数。

子查询参数化必须独立处理
子查询本身不是安全漏洞,但一旦用字符串拼接方式嵌入外部输入,就和主查询一样危险。常见错误是只对主SQL用了PreparedStatement,却把子查询当“静态结构”直接拼进去。
比如 Java 中这样写就是高危的:
String subQuery = "SELECT id FROM logs WHERE ip = '" + userInputIp + "'";
String sql = "SELECT * FROM users WHERE id IN (" + subQuery + ")";
// 即使外层用 PreparedStatement,subQuery 已经被污染
- 子查询中的
userInputIp没经过任何绑定,单引号、括号、分号全可逃逸 - 数据库执行时会先解析整个字符串,
PreparedStatement的占位符机制对拼进去的子串完全无效 - 尤其在 MySQL 中,子查询支持多语句(如
;分隔),堆叠注入风险比主查询更高
MyBatis 里 #{} 不自动保护子查询
很多人误以为 MyBatis 的 #{} 能兜住所有 SQL 片段,其实它只作用于该占位符所在位置——如果子查询是通过字符串拼接到 SQL 模板里的,#{} 就不生效。
错误示范:
<select id="getUserByLogIp" resulttype="User">
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM logs WHERE ip = '${ip}' <!-- 这里用 ${} 直接拼接,危险!-->
)
</select>
-
${}是字符串替换,等同于 Java 的+拼接,' OR '1'='1会原样进入子查询 - 即使主查询用
#{},子查询里用了${},整条语句仍不可信 - 正确做法是把子查询也改造成独立的参数化语句,或用
<foreach></foreach>配合预编译集合参数
存储过程调用子查询时权限要收敛
用存储过程封装子查询看似安全,但如果过程内部用了动态 SQL(如 CONCAT() 或 EXECUTE IMMEDIATE),且参数未校验,照样会中招。
MySQL 存储过程中典型陷阱:
DELIMITER //
CREATE PROCEDURE GetUserByIp(IN ip_input VARCHAR(45))
BEGIN
SET @sql = CONCAT("SELECT * FROM users WHERE id IN (
SELECT user_id FROM logs WHERE ip = '", ip_input, "')");
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
-
CONCAT()和PREPARE组合等于手动绕过预编译,ip_input完全裸奔 - 即使调用方用了
CallableStatement绑定参数,过程内动态拼接阶段已失去控制 - 真正安全的做法是:子查询部分也用
?占位,并在EXECUTE stmt USING ?中传参
布尔盲注下子查询更难检测
子查询常被用于布尔盲注构造(如 (SELECT COUNT(*) FROM admins) > 0),这类场景下传统 WAF 或日志关键词过滤容易漏掉——因为不报错、不回显、无明显 SQL 关键字。
- 攻击者用子查询做条件探测时,请求体可能只含合法字符,但响应时间或状态码有规律性差异
- 应用层若没对子查询涉及的字段做白名单限制(如只允许查
logs.ip,不允许查admins.password),就等于开了后门 - 最稳妥的缓解方式:业务上能不用子查询就不用;必须用时,把子查询逻辑收归 DAO 层统一参数化,禁止在 Service 或 Controller 层拼接
子查询的危险性不在语法本身,而在于它容易成为参数化链条上的断裂点——开发者常默认“子查询是数据库内部逻辑”,忽略其输入来源同样需要隔离与校验。










