位置:首页 > SQL > SQL 子查询如何防止 SQL 注入风险

SQL 子查询如何防止 SQL 注入风险

时间:2026-08-22  |  作者:星际追番人  |  阅读:0

目录

  1. 子查询的风险点到底在哪里
  2. 哪些位置不能用参数化代替
  3. 哪些值必须绑定到最内层子查询
  4. MyBatis 里为什么 ${} 在子查询中更危险
  5. 实际排查时抓住这两个检查点

前言

很多人把子查询、嵌套查询和 SQL 注入风险直接画上等号,但真正需要警惕的并不是查询层级,而是用户输入有没有被当作 SQL 结构或原样字符串拼进去。下面从不能参数化的位置、必须绑定的值,以及 MyBatis 的常见误用三部分拆开讲,帮你快速判断一段子查询 SQL 到底安不安全。

很多人一看到子查询、嵌套查询,就会下意识担心 SQL 注入更难防。其实问题不在子查询本身,而在你是不是把用户输入直接拼进了 SQL。本文把“哪些位置根本不能参数化”“哪些值必须一路绑定到最内层”以及 MyBatis 里最常见的误区拆开讲清楚,方便你快速判断现有写法到底安全不安全。

子查询的风险点到底在哪里

先说结论:子查询本身不会引发 SQL 注入风险,真正危险的是把用户输入拼接到子查询字符串里。只要所有用户可控的值都走参数化绑定,子查询和主查询在安全性上没有本质区别。

实际排查时,可以把动态内容分成两类:

  • 结构化内容:表名、列名、ORDER BY 字段、分组表达式等,这类内容决定 SQL 语法结构,不能直接靠参数占位符替代。
  • 普通值:数字、字符串、日期、状态值等,这类内容必须使用参数化绑定。

只要这两类边界没分清,子查询写得越深,越容易在某一层漏掉校验或绑定。

哪些位置不能用参数化代替

数据库在预编译阶段必须先确定 SQL 语法结构,因此有些位置不能使用 :param 来代入。

展示 SQL 子查询中哪些部分不能参数化、哪些部分必须白名单控制的白底信息图。
子查询中不能参数化的动态位置把“不能参数化的结构”与“可绑定的值”分开看,更容易定位真正的风险点。
  • 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 判断传入值是否合法,再决定是否拼入 SQL。这里的核心不是转义,而是限制可选范围。

哪些值必须绑定到最内层子查询

凡是用户可控的,无论出现在主查询还是嵌套多层的子查询中,都必须参数化绑定,包括数字、字符串、日期等。

下面这类写法就有明显风险。

"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 子句是一个常见误区。子查询返回多行,不代表你可以把外部输入直接拼成一个列表字符串。参数化一次仍然只处理一个值;如果要动态构造 IN (, , ),必须按实际数量生成占位符,并把每个值分别绑定。

换句话说,不能靠字符串拼接占位符,也不能把一整段外部输入当成“现成的 IN 列表”塞进 SQL。

MyBatis 里为什么 ${} 在子查询中更危险

MyBatis 中的 ${} 本质上是字符串替换,不是参数化。它不管出现在主 SQL 还是子查询里,都会把内容直接拼接进去,因此一直是注入高发区。

展示 MyBatis 子查询中 ${} 与 #{} 差异及注入路径的白底信息图。
MyBatis 子查询参数写法对比在 MyBatis 里,子查询不会放大风险,真正放大风险的是把 ${} 当成参数化来用。

典型危险示例如下:

如果攻击者传入 type=abc' UNION SELECT password FROM users --,就可能把原查询结构改写,造成数据泄露。

更稳妥的写法是什么

这类场景应一律改用 #{},并确保传入的是简单类型,例如 StringInteger,不要传表达式或 SQL 片段。

还要注意一点:#{} 在子查询中同样生效,但 MyBatis 只负责把值安全转义,不会帮你检查子查询结构本身是否合法。也就是说,凡是动态表名、列名、排序字段这类结构化部分,仍然要靠白名单控制。

实际排查时抓住这两个检查点

判断一段带子查询的 SQL 是否安全,通常只要按下面两个问题去看:

  • 所有结构化动态点,比如表名、列名、ORDER BY 字段、分组表达式,是否都做了白名单校验。
  • 所有用户可控的值,是否都通过参数绑定传入,而且绑定覆盖到了最内层子查询。

最容易出问题的地方恰恰是“只检查了外层,漏掉了里层”。只要任意一层重新回到字符串拼接,注入风险就会立刻回来。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多