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

SELECT 语句里的子查询本身不引入注入风险,真正危险的是把用户输入拼进子查询字符串里。只要用参数化方式传入值,子查询和主查询一样安全。
子查询里不能用 ? 或 :param 的地方有哪些
子查询的结构部分(如表名、列名、ORDER BY 字段、GROUP BY 表达式)无法用参数占位符,因为数据库在预编译阶段需要明确这些语法成分。
-
SELECT * FROM (SELECT id, name 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 不会帮你校验子查询结构是否合法——它只保证你传进去的值被安全转义。
带 EXISTS 或 NOT EXISTS 的子查询怎么防
EXISTS 本身不带值参与比较,但它的子查询内部仍可能拼接用户输入。
- 错误:
SELECT * FROM products WHERE EXISTS (SELECT 1 FROM reviews WHERE product_id = products.id AND comment LIKE '%${keyword}%') - 正确:
SELECT * FROM products WHERE EXISTS (SELECT 1 FROM reviews WHERE product_id = products.id AND comment LIKE CONCAT('%', ?, '%'))
注意:LIKE 模糊匹配必须把通配符(%)写在 SQL 里,而不是拼到参数值里;否则用户传 %admin% 就可能绕过过滤逻辑。参数值应为干净的原始输入,如 admin。
子查询不是“免疫区”,它只是 SQL 的一种结构形式。漏洞不出在“有没有子查询”,而出在“有没有把用户输入当代码用”。最容易被忽略的,是那些藏在括号深处、看起来很“静态”的子查询——一旦里面混进了 ${} 或手动拼接,风险一点不比主查询低。











