子查询中所有值位置必须用#{}参数化防注入,而表名、列名、ORDER BY等结构部分无法参数化,须白名单校验后用${}拼接;IN列表需动态生成占位符,严禁字符串拼接。

子查询里的参数绑定和主查询完全一致——所有用户可控的值必须走参数化,但结构部分(表名、列名、ORDER BY)永远不能用占位符。
子查询中哪些地方能用 ? 或 #{}
只要位置是“值”,就支持参数化,无论嵌套多深:
-
WHERE条件中的值:SELECT * FROM users WHERE id IN (SELECT user_id FROM logs WHERE level = ?) -
IN子句单个元素:WHERE status IN (?, ?, ?)(注意:占位符数量必须与实际值一一对应) -
JOIN条件右侧的值:JOIN roles r ON r.id = (SELECT role_id FROM user_meta WHERE user_id = ?) - MyBatis 的
#{}在子查询里照常生效,比如:WHERE id IN (SELECT user_id FROM logs WHERE type = #{type})
错误做法是把整个子查询当字符串拼进去,比如 PHP 里写 "SELECT * FROM users WHERE id IN (" . $subSql . ")" —— 这等于放弃所有防御。
子查询里哪些地方绝对不能用 ? 或 #{}
数据库在预编译阶段就需要确定语法结构,所以以下内容无法参数化,硬上会报错:
- 表名:
SELECT * FROM (SELECT id FROM ?) t→ 错误,?不能代入表名 - 列名:
SELECT * FROM users WHERE id IN (SELECT ? FROM logs)→ 错误,?不能代入列名 -
ORDER BY字段:SELECT * FROM (SELECT id, name FROM users) t ORDER BY ?→ 错误,即使子查询外也不行 -
GROUP BY表达式、LIMIT数值、UNION分支结构等同理
这类问题必须靠白名单校验后拼接,比如允许排序字段仅限 ['id', 'created_at', 'status'],再映射到真实列名,最后拼成 ORDER BY created_at DESC。
IN 列表动态长度怎么安全处理
不能指望数据库自动展开数组,WHERE id IN (?) 永远只匹配一个值,哪怕你传入的是列表。
- 前端或服务端先确定 ID 数量,比如 [101, 205, 309] → 生成三个占位符:
WHERE id IN (?, ?, ?) - SQL 模板里写死对应数量的
?,再逐个绑定值(别用AddWithValue,显式指定类型) - 数量不确定时,避免字符串拼接占位符;改用临时表 + 表值参数(SQL Server)、
UNNEST(PostgreSQL)或拆成多次查询 - Hibernate 用户注意:
setParameter("ids", list)是错的,必须用setParameterList("ids", list)
MyBatis 里 ${} 在子查询中有多危险
${} 是字符串替换,不是参数化。它出现在子查询里,和出现在主 SQL 里一样致命:
- 危险示例:
<select>SELECT * FROM users WHERE id IN (SELECT user_id FROM logs WHERE type = '${type}')</select> - 攻击者传
type=abc' UNION SELECT password FROM users --,就能直接拖库 - 正确做法:一律改用
#{},且确保传入的是简单类型(String、Integer),不传表达式或 SQL 片段 - 特别提醒:
#{}只保证值被转义,不校验子查询结构是否合法——白名单校验仍需你手动做
真正容易被忽略的是:子查询嵌套超过两层时,执行计划可能退化(MySQL 8.0+ 倾向物化临时表,PostgreSQL 可能走 Nested Loop),性能断崖下跌,但注入风险一点没少——安全和性能得同步盯紧。

















