当前位置: 首页 > news >正文

为啥预编译SQL能够防止SQL注入

前言

之前我一个搞网络安全的朋友问了我一个的问题,为啥用 PreparedStatement 预编译的 SQL 就不会有被 SQL 注入的风险?

第一时间我联想到的是八股文中关于 Mybatis 的脚本 ${}#{} 的问题,不过再想想,为啥 ${} 会有 SQL 注入的风险,而 #{} 就没有?是因为到 PreparedStatement 做了什么处理吗?不知道。

然后我又想了想,预编译到底是个什么概念?预编译或者不预编译的 SQL 对数据库来说有什么区别吗?PreparedStatement 又在这个过程中扮演了怎样的角色?不知道。

好吧,我发现我确实对这个问题一无所知,看来需要亲自研究一下了。

一、数据库预编译

当我们说到关于持久层框架的功能,必然需要先想想这个功能的源头到底是不是直接通过数据库提供的。实际上和事务一样,SQL 预编译的功能也是需要数据库提供底层支持的。

1、预编译SQL的用法

以 MySQL 为例,在 MySQL 中,所谓预编译其实是指先提交带占位符的 SQL 模板,然后为其指定一个 key,MySQL 先将其编译好,然后用户再拿着 key 和占位符对应的参数让 MySQL 去执行,用法有点像 python 中的 format 函数。

一个标准的预编译 SQL 的用法如下:

prepare prepare_query from'select * from s_user where username = ?' # 提交带有占位符的参数化 SQL,也可以理解为 SQL 模板
set@name='%王五'; # 指定一个参数
execute prepare_query using@name; # 指定参数化 SQL 的 key 和参数,让 MySQL 自己去拼接执行

先通过 prepare 设置一个 SQL 模板,然后通过 execute 提交参数,MySQL 会自行根据参数替换占位符,到最后执行的 SQL 就是:

select*from s_user where username ='%王五'

2、预编译的原理

这里有个有意思问题,按网上的说法,prepare 执行的时候实际上 SQL 已经编译完了,所以可以防止注入,因为后续不管塞什么参数都不可能在调整语法树了,换个角度想,这是不是说明,如果我们一开始就让 prepare 执行的 SQL 模板的关键字变成占位符,是不是应该在这个时候就编译不通过?

比如,可以把查询的表名改成占位符:

prepare prepare_query from'select * from ? where username = ?'# >1064- You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to 
# use near '? where username = ?'at line 1

实际上也确实不行,因为编译时必须确定主表,因此在 from 后面加占位符会导致预编译不通过。

那么只在查询字段里面套一个嵌套查询呢?

prepare prepare_query from'select ? from s_user';
SET@c='(select * from s_user) as q';
EXECUTE prepare_query using@c;# 查询结果
# (select*from s_user) as q
# (select*from s_user) as q
# (select*from s_user) as q
# ......

查询成功了,不过得到的结果的固定的 (select * from s_user) 这个字符串,我们检查一下 MySQL 的执行日志,看看最终执行的 SQL 变成什么样了:

Prepare	select ? from s_user
Query	SET@c='(select * from s_user) as q'
Query	EXECUTE prepare_query using@cExecute	select'(select * from s_user) as q'from s_user # 最终执行的SQL

显然,(select * from s_user) 参数本身被直接转义为了一串普通的字符串,我们试图“注入”的 SQL 片段完全不会生效

换而言之,对于预编译 SQL 来说,我们作为模板的参数化 SQL 已经完成的编译过程,这段 SQL 包含几条有效语句?查哪张表?查哪些字段?作为条件的字段有哪些?......这些在 prepare 语句执行完后都是固定的,此后我们再通过 execute 语句塞进去的任何参数,都会进行转义,不会再作为 SQL 的一部分。这就是为什么说预编译 SQL 可以防止注入的原因。

二、JDBC的预编译

现在我们知道了预编译在数据库中是个怎样的功能,那么 JDBC 又是如何把这个功能提供给开发者使用的呢?

1、PreparedStatement

从最开始学 JDBC 时,我们就知道通过 JDBC 连接数据库一般是这样写的:

Class.forName(JDBC_DRIVER); // 加载驱动Connectionconnection= DriverManager.getConnection(URL, USERNAME, PASSWORD); // 获取连接PreparedStatementpreparedStatement= connection.prepareStatement(sql); // 获取sqlStatement
preparedStatement.setString(1, foo); // 设置参数ResultSetresultSet= preparedStatement.executeQuery(); // 执行SQL

这里有一个关键角色 PreparedStatement,相比起它的父接口 Statement,它最大的变化是多了各种格式为 setXXX 的、用于设置与占位符对应的参数的方法,显然它正对应着上文我们提到的预编译 SQL。

2、虚假的“预编译”

不过事情显然没有这么简单,我们依然以 MySQL 为例,默认情况下 MySQL 驱动包提供的 PreparedStatement 实现类 ClientPreparedStatement 也能起到防止 SQL 注入的功能,但是方式跟我们想的不太一样。

假设现有如下代码,我们尝试模拟进行一次 SQL 注入:

Stringsql="select * from s_user where username = ?";
PreparedStatementpreparedStatement= connection.prepareStatement(sql);
preparedStatement.setString(1, "王五' union select * from s_user");
ResultSetresultSet= preparedStatement.executeQuery();

运行上述代码并正常的请求数据库,然后我们去数据库执行日志中查看对应的执行的 SQL 如下,会发现只有这么一行:

Query select*from s_user where username ='王五'' union select * from s_user'

显然跟我们上文说到的先 prepareexecute 流程不同,带有占位符的原始 SQL 模板并没有在日志中出现,但是代码中的 王五' 确实也被转义为了 '王五''

数据库到底收到了哪些数据?

那么数据库到底拿到的就是这条 SQL,还是原始的 SQL 模板 + 参数呢?

为了了解这一点,我们打断点跟踪 ClientPreparedStatement.executeQuery 方法,一路找到它组装请求数据库的参数的那一行代码:

MessagesendPacket= ((PreparedQuery<?>) this.query).fillSendPacket();

最后我们会进入 AbstractPreparedQuery.fillSendPacket 这个方法,这里主要干的事是把我们带占位符的原始 SQL 模板和参数合并为最终要执行的 SQL ,并封装到 NativePacketPayload 对象,用于在后续发起 TCP 请求时把 SQL 参数转为二进制数据包。

为了验证这一点,我们先拿到 sendPacket 对象,再获取里面的字节数组,最后转为字符串:

可以看到内容就是已经格式化完的 SQL:

select*from s_user where username ='王五'' union select * from s_user'

现在答案就很明显了,转义在 preparedStatement.setString 方法调用的时候完成,而 PreparedStatement发起请求前就把转义后的参数和 SQL 模板进行了格式化,最后发送到 MySQL 的时候就是一条普通的 SQL

鉴于此,我们可以说 MySQL 提供的 PreparedStatement 在默认情况下是假的“预编译”,它只不过在设置参数的时候帮我们对参数做了一下转义,但是最后发送到数据库的依然是普通的 SQL,而不是按预编译 SQL 的方式去执行。

3、真正的预编译

好吧,那既然 MySQL 提供了这个预编译的功能,那通过 JDBC 肯定也还是有办法用上真正的预编译功能的,实际上要做到这点也很简单,就是直接在驱动的 url 上配上 useServerPrepStmts=true ,这样就会真正的启用 MySQL 的预编译功能。

依然以上文的代码为例:

Stringsql="select * from s_user where username = ?";
PreparedStatementpreparedStatement= connection.prepareStatement(sql);
preparedStatement.setString(1, "王五' union select * from s_user");
ResultSetresultSet= preparedStatement.executeQuery();

设置了 useServerPrepStmts=true 后再执行代码,去数据库查看执行日志有:

Executeselect*from s_user where username ='王五\'unionselect*from s_user'
Prepare select * from s_user where username = ?

此时 MySQL 的预编译功能就真正的生效了。

我们回到 ClientPreparedStatement.executeQuery 创建 sendPacket 地方看,此时通过 ((PreparedQuery<?>) this.query).fillSendPacket(); 拿到的 Message 对象是 null,然后进一步追踪到最后向 MySQL 发送请求的地方 NativeSession.execSQL

public <T extendsResultset> T execSQL(Query callingQuery, String query, int maxRows, NativePacketPayload packet, boolean streamResults,ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory, ColumnDefinition cachedMetadata, boolean isBatch) {// ... ...try {// 如果 sendPacket 为 null,则调用 sendQueryString 方法,把原始 sql 和参数序列化为二进制数据包returnpacket== null? ((NativeProtocol) this.protocol).sendQueryString(callingQuery, query, this.characterEncoding.getValue(), maxRows, streamResults, cachedMetadata, resultSetFactory)// 否则调用 sendQueryPacket 方法,直接发送数据包: ((NativeProtocol) this.protocol).sendQueryPacket(callingQuery, packet, maxRows, streamResults, cachedMetadata, resultSetFactory);}// ... ...}

更具体的实现就不看了,基本都是关于序列化请求参数的逻辑。

三、Myabtis占位符与预编译

至此问题真相大白了,不过还是顺带扯一下八股文常提到的 Mybatis 占位符 #{}${} 是如何影响 SQL 注入问题的。

当然,看完上面的内容其实就已经很好猜到原因了:

  • #{} 对应的内容会作为 SQL 参数的一部分通过 PreparedStatement.setXXX 装入请求;

  • ${} 对应的内容会直接作为 SQL 模板的一部分,而不会视为独立的请求参数;

在 Mybatis 中,用于解析占位符的类为 GenericTokenParser ,根据它我们很容易在源码中找到占位符的处理方法,从而验证我们的猜想:

其中,#{} 占位符在 SqlSourceBuilder.ParameterMappingTokenHandler.handleToken 方法中处理:

public String handleToken(String content) {parameterMappings.add(buildParameterMapping(content));return"?";
}

可见 #{} 占位符会被解析为 ? 占位符,而对于的数据会被添加到 parameterMappings 用于后续塞到 PreparedStatement

${} 占位符在 PropertyParser.VariableTokenHandler.handleToken 方法中被处理:

public String handleToken(String content) {if (variables != null) {Stringkey= content;if (enableDefaultValue) {finalintseparatorIndex= content.indexOf(defaultValueSeparator);StringdefaultValue=null;if (separatorIndex >= 0) {key = content.substring(0, separatorIndex);defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());}if (defaultValue != null) {return variables.getProperty(key, defaultValue);}}if (variables.containsKey(key)) {return variables.getProperty(key);}}return"${" + content + "}";
}

若占位符符合规范,则占会根据占位符中的内容去用户给定的参数中取值,并且让值直接替换掉原本 SQL 脚本中的 ${} 占位符。

这就是“ Mybatis#{} 而不是 ${} 可以防止 SQL 注入的真相

总结

回顾一下全文,当我们说“预编译”的时候,其实这个功能来自于数据库的支持,它的原理是先编译带有占位符的 SQL 模板,然后在传入参数让数据库自动替换 SQL 中占位符并执行,在这个过程中,由于预编译好的 SQL 模板本身语法已经定死,因此后续所有参数都会被视为不可执行的非 SQL 片段被转义,因此能够防止 SQL 注入。

当我们通过 JDBC 使用 PreparedStatement 执行预编译 SQL 的时候,此处的预编译实际上是假的预编译(至少 MySQL 是如此,不过其他数据库仍待确认),PreparedStatement 只是在设置参数的时候自动做了一层转义,最终提交给数据库执行的 SQL 仍然是单条的非预编译 SQL。

而当我们通过在驱动 url 上开启 useServerPrepStmts 配置后,预编译就会真正的生效,驱动包发往数据库的请求就会分成带占位符的 SQL 模板和参数,到了数据库再由数据库完成格式化并执行。

此外,八股文常提到的“Mybatis#{} 相比 ${} 可以防止 SQL 注入”这一点,本质上是因为 #{} 占位符会被解析为 SQL 模板中的 ? 占位符,而 ${} 占位符会被直接解析为 SQL 模板的一部分导致的。

最后脑补一下,由于 useServerPrepStmts 不开启时 PreparedStatement 的预编译实际上是假的预编译,所以理论上使用 #{} 也并非绝对安全,如果有办法绕过 PreparedStatement 的检查,那么数据库拿到被注入过的 SQL 直接执行,依然有暴毙的风险。

相关文章:

为啥预编译SQL能够防止SQL注入

前言之前我一个搞网络安全的朋友问了我一个的问题&#xff0c;为啥用 PreparedStatement 预编译的 SQL 就不会有被 SQL 注入的风险&#xff1f;第一时间我联想到的是八股文中关于 Mybatis 的脚本 ${} 和 #{} 的问题&#xff0c;不过再想想&#xff0c;为啥 ${} 会有 SQL 注入的…...

IGKBoard(imx6ull)-SPI接口编程-回环测试

文章目录1- 使能imx6ull开发板SPI驱动2- 回环测试imx6ull开发板物理连接3- 编程SPI回环测试4- 代码重难点分析&#xff08;1&#xff09;spi_device结构体&#xff08;2&#xff09;spi_ioc_transfer结构体&#xff08;3&#xff09;ioctl函数对于SIP不了解的可以参考这篇文章&…...

Python基础学习10——类

基本概念 面向对象编程是最有效的软件编写方法之一。理解面向对象编程可以让你像程序员那样从一个更加宏观的角度去理解代码&#xff0c;并大大提高程序员之间的合作效率。面向对象编程的两个核心概念是对象和类 **对象&#xff1a;**对象是能帮助我们解决问题的具体东西。比如…...

项目实战典型案例14——代码结构混乱 逻辑边界不清晰 页面美观设计不足

代码结构混乱 逻辑边界不清晰 页面美观设计不足一&#xff1a;背景介绍问题1 代码可读性差&#xff0c;代码结构混乱问题2 逻辑边界不清晰&#xff0c;封装意识缺乏示例3.展示效果上的美观设计二&#xff1a;思路&方案问题一&#xff0c;代码可读性差&#xff0c;代码结构混…...

SpringBoot 读取自定义Properties参数

目录 1. 概述 2. 实现方式 2.1 Value方式 2.2 PropertySource与ConfigurationProperties相结合 3. 结束 1. 概述 最近想尝试写一个定时任务管理&#xff0c;相关参数不想在Spring的配置文件Application.yml或者Application.properties获取。想自己新建一个properties文件。顺…...

机器学习100天(三十七):037 朴素贝叶斯-挑个好西瓜!

《机器学习100天》完整目录:目录 机器学习100天,今天讲的是:朴素贝叶斯-挑个好西瓜! 红色石头已经了解了贝叶斯定理和朴素贝叶斯法,接下来已经可以很自信地去买瓜了。买瓜之前,还有一件事情要做,就是搜集样本数据。红色石头通过网上资料和查阅,获得了一组包含 10 组样…...

c#遍历窗口,根据标题获取handle并显示窗口

using System.Runtime.InteropServices;using System.Text;//1&#xff0c;定义//[DllImport("User32.dll", EntryPoint "FindWindow")]//public extern static IntPtr FindWindow(string lpClassName, string lpWindowName);[DllImport("user32.dll…...

MyBatis高频面试专题

一、介绍下MyBatis中的工作原理 1。介绍MyBatis的基本情况&#xff1a;ORM 2。原理&#xff1a; MyBatis框架的初始化操作处理SQL请求的流程 1.系统启动的时候会加载解析全局配置文件和对应映射文件。加载解析的相关信息存储在 Configuration 对象 Testpublic void test1(…...

曹云金郭德纲关系迎曙光,新剧《猎黑行动》被德云社弟子齐点赞

话说天下大势&#xff0c;分久必合&#xff0c;合久必分。这句话经过了历史的证明&#xff0c;如今依然感觉非常实用。 就拿郭德纲和曹云金来说&#xff0c;曾经后者是前者的得门生&#xff0c;两个人不但情同父子&#xff0c;曹云金还是郭德纲默认接班人。然而随着时间的流逝&…...

如何在 OpenEuler 系统中安装 Docker

Docker 是一种流行的开源容器化平台&#xff0c;它能够将应用程序与其依赖项打包成可移植的容器&#xff0c;从而简化了应用程序的部署和管理。本文将介绍在 OpenEuler 系统中安装 Docker 并使用 Docker 容器控制 5G 模块的具体步骤。 安装 Docker 安装 Docker 的具体步骤如下…...

MySQL日志管理

日志管理在数据库保存数据时&#xff0c;有时候不可避免会出现数据丢失或者被破坏&#xff0c;这种时候&#xff0c;我们必须保证数据的安全性和完整性&#xff0c;就需要使用日志来查看或者恢复数据了数据库中数据丢失或被破坏可能原因&#xff1a;误删除数据库数据库工作时&a…...

进 制

进制进制一、进制概念二、进制的转换三、二进制的运算3.1 与运算3.2 或运算3.3 非运算3.4 异或运算3.5 位运算&#xff08;位移&#xff09;四、原码、反码、补码4.1 原码4.2 反码4.3 补码五、浮点数十进制转换成二进制进制 一、进制概念 十进制是指逢十进一。 计算机中二进制…...

pycharm关联github、新建以及更新仓

此处已经默认你安装了git以及pycharm,这篇文章将会教给大家如何利用pycharm管理自己的github. 目录 pycharm关联github设置 Github创建新的仓 仓库的更新 pycharm:2022。不同版本界面略有不同。 pycharm关联github设置 设置PyCharm&#xff0c;打开File --> Settings -…...

java基础知识之小碎片(自问自答版本)---嘻嘻,春招加油呀~

1.public/private/protected/default的区别&#xff1f; public:对所有类可见 private 只有类本身可以访问&#xff0c;其他类想访问可以通过该类的成员方法访问如getter/setter protected:对同一包内的类和所有子类可见 default:在同一包内可见&#xff0c;不加修饰符 2.jav…...

蚁群算法c++

//轮盘赌选择下一步行进城市 int citySelect(int k, int f) { int c 0;//记录蚂蚁可行进的城市个数 //1、计算可行进的各城市 选择概率 for (int m 0; m < cityNum; m) { //若城市&#xff08;i,j&#xff09;之间有路且j不在蚂蚁k的禁忌表中&#xf…...

北大青鸟天府校区IT学习大揭秘

口罩已放开&#xff0c;一切都要重新出发。 开年才一个多月&#xff0c;已经有很多小伙伴想培训转行IT行业或者已经在咨询、报名培训IT技术。作为老牌培训机构&#xff0c;也有很多小伙伴相信我们&#xff0c;选择了我们。很感谢大家的信任&#xff0c;作为老牌培训机构&#…...

04 Linux errno.h错误码中文注释

Linux错误码中文注释 作者将狼才鲸创建日期2023-03-04/******************************************************************************* \brief 错误码* \note 基于linux_6.1-rc4\include\uapi\asm-generic\errno-base.h* linux_6.1-rc4\tools\arch\alpha\include\uapi…...

MySQL表的约束

文章目录表的约束空属性默认值列描述zerofill主键自增长唯一键外键表的约束 真正约束字段的是数据类型&#xff0c;但是数据类型约束很单一&#xff0c;需要有一些额外的约束&#xff0c;更好的保证数据的合法性&#xff0c;从业务逻辑角度保证数据的正确性。比如说我们的居民…...

Go语言的条件控制语句及循环语句的学习笔记

一、Go的条件控制语句 Go 语言提供了以下几种条件判断语句&#xff1a; 语句描述if 语句if 语句 由一个布尔表达式后紧跟一个或多个语句组成。if…else 语句if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。if 嵌套语句你可以在 if 或…...

D. Linguistics(思维 + 贪心)

Problem - D - Codeforces Alina发现了一种奇怪的语言&#xff0c;它只有4个单词:a, B, AB, BA。事实也证明&#xff0c;在这种语言中没有空格:一个句子是通过将单词连接成一个字符串来写的。Alina发现了一个这样的句子&#xff0c;她很好奇:有没有可能它恰好由a个单词a, b个单…...

云计算——弹性云计算器(ECS)

弹性云服务器&#xff1a;ECS 概述 云计算重构了ICT系统&#xff0c;云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台&#xff0c;包含如下主要概念。 ECS&#xff08;Elastic Cloud Server&#xff09;&#xff1a;即弹性云服务器&#xff0c;是云计算…...

树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频

使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

1.3 VSCode安装与环境配置

进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件&#xff0c;然后打开终端&#xff0c;进入下载文件夹&#xff0c;键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!

5月28日&#xff0c;中天合创屋面分布式光伏发电项目顺利并网发电&#xff0c;该项目位于内蒙古自治区鄂尔多斯市乌审旗&#xff0c;项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站&#xff0c;总装机容量为9.96MWp。 项目投运后&#xff0c;每年可节约标煤3670…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解

本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”

2025年#高考 将在近日拉开帷幕&#xff0c;#AI 监考一度冲上热搜。当AI深度融入高考&#xff0c;#时间同步 不再是辅助功能&#xff0c;而是决定AI监考系统成败的“生命线”。 AI亮相2025高考&#xff0c;40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕&#xff0c;江西、…...

初探Service服务发现机制

1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能&#xff1a;服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源&#xf…...

音视频——I2S 协议详解

I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议&#xff0c;专门用于在数字音频设备之间传输数字音频数据。它由飞利浦&#xff08;Philips&#xff09;公司开发&#xff0c;以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...

站群服务器的应用场景都有哪些?

站群服务器主要是为了多个网站的托管和管理所设计的&#xff0c;可以通过集中管理和高效资源的分配&#xff0c;来支持多个独立的网站同时运行&#xff0c;让每一个网站都可以分配到独立的IP地址&#xff0c;避免出现IP关联的风险&#xff0c;用户还可以通过控制面板进行管理功…...

快刀集(1): 一刀斩断视频片头广告

一刀流&#xff1a;用一个简单脚本&#xff0c;秒杀视频片头广告&#xff0c;还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农&#xff0c;平时写代码之余看看电影、补补片&#xff0c;是再正常不过的事。 电影嘛&#xff0c;要沉浸&#xff0c;…...