子查询本身不引入SQL注入风险,真正危险的是将用户输入拼接到子查询字符串中;表名、列名、ORDER BY字段等结构化内容须白名单校验,所有用户可控的值(含嵌套子查询中的值)必须严格参数化绑定。

子查询本身不会引入SQL注入风险,真正危险的是把用户输入拼进子查询字符串里;只要所有用户可控的值都走参数化绑定,子查询和主查询一样安全。
子查询里哪些地方不能用 ? 或 :param
数据库在预编译阶段必须明确语法结构,所以以下位置无法参数化:
-
SELECT * FROM (SELECT id FROM ?) AS t→?不能代入表名 -
SELECT * FROM users WHERE id IN (SELECT ? FROM logs)→?不能代入列名 -
SELECT * FROM users ORDER BY ?→?即使出现在子查询外,ORDER BY后也不支持参数化
这类结构化内容(表名、列名、排序字段、分组表达式)必须走白名单校验。比如允许排序字段只能是 ['id', 'name', 'created_at'],再用 in 判断传入值是否合法。
子查询中哪些值必须参数化
所有用户可控的**值**——无论嵌套多深——都必须绑定参数,包括数字、字符串、日期等:
- 错误写法(PHP + MySQLi):
"SELECT * FROM users WHERE id IN (SELECT user_id FROM logs WHERE level = '" . $_GET['level'] . "')" - 正确写法:
"SELECT * FROM users WHERE id IN (SELECT user_id FROM logs WHERE level = ?)",然后bind_param("s", $level) - Python + PyMySQL:
cursor.execute("SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE status = %s)", (status,))
注意:IN 子句返回多行时,参数化仍只处理单个值;若需动态构造 IN (?, ?, ?),必须按实际数量生成占位符并一一绑定,不能靠字符串拼接占位符。
MyBatis 中 ${} 在子查询里为什么特别危险
${} 是字符串替换,不是参数化,无论出现在主 SQL 还是子查询里,都会直接拼接——这是注入高发区:
- 危险示例:
<select id="search">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 片段
#{} 在子查询中也生效,但 MyBatis 不会帮你校验子查询结构是否合法——它只保证你传进去的值被安全转义。
最容易被忽略的是:白名单校验必须覆盖所有结构化动态点(比如表名、列名、ORDER BY 字段),而参数化必须穿透到最内层子查询的每一个值。漏掉任意一层,风险就回来了。

















