Java中的SQL注入简易分析

本文深入探讨了JDBC接口如何通过预编译防止SQL注入,详细分析了PreparedStatement与Statement的区别。同时,介绍了MyBatis在处理SQL时的动态和静态SqlSource,以及如何正确使用like和orderby避免注入风险。预编译在MySQL中的开启与关闭条件也进行了说明,并给出了MyBatis中避免SQL注入的最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JDBC

什么是JDBC?JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。

使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐

│  ┌───────────────┐  │
   │   Java App    │
│  └───────────────┘  │
           │
│          ▼          │
   ┌───────────────┐
│  │JDBC Interface │<─┼─── JDK
   └───────────────┘
│          │          │
           ▼
│  ┌───────────────┐  │
   │  JDBC Driver  │<───── Vendor
│  └───────────────┘  │
           │
└ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┘
           ▼
   ┌───────────────┐
   │   Database    │
   └───────────────┘

通过SQLInjection.java代码对于JDBC中对于数据的调用使用预编译和不使用预编译两种情况进行分析

不使用占位符拼接

Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement stmt = conn.createStatement();
String sql = "select name from students where id =" + value;
ResultSet rs = stmt.executeQuery(sql);

不使用占位符拼接的关键代码如上,先通过Connection提供的createStatement()方法创建一个stmt对象,用于执行一个查询.然后执行stmt对象提供的executeQuery(sql)传入我们构造的SQL语句并获得返回的结果集,使用ResultSet来引用结果集.

而在执行的关键代码中,先是把sql语句传入Statementlmpl.class当中的ResultSet executeQuery()方法中,在locallyScopedConn.execSQL()中执行SQL并将执行结果放入到this.result
请添加图片描述

请添加图片描述

通过追踪execSQL()方法可以追溯到Connectionlmpl.class文件,我们可以看到在将sql语句接收到方法中后,将语句交由MysqlIO来执行.
请添加图片描述

请添加图片描述

查看sqlQueryDirect()方法,通过拼装发送包信息,最后通过Buffer resultPacket = this.sendCommand(3, (String)null, queryPacket, false, (String)null, 0);中的sendCommand()方法将其发送出去
请添加图片描述

请添加图片描述

纵观在Statementlmpl.class当中的ResultSet executeQuery()方法中只是将我们的sql语句进行一步步的传递,大部分只进行了功能上的校验,在最后发送到数据库进行执行,通过names列表可以看到数据库中所有的名字都被读取了出来.
请添加图片描述

使用占位符(PreparedStatement)

String sql = "select name from students where id =?";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.setString(1, value);
ResultSet resultSet = preparedStatement.executeQuery();

使用PreparedStatement预编译方法,对于要传递的id的值先使用?进行占位,并且把数据连同sql本身传给数据库,以此保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同.

我们传递数据1 or 1=1进行测试,以此来学习在PrepareStatement对于我们传入的数据的处理过程.

PrepareStatement的开启与关闭情况

在对prepareStatement()方法进行调试的时候,我们需要了解一个关于预编译的知识点.

预编译功能跟MySQL版本及 MySQL Connector/J(JDBC驱动)版本都有关,首先MySQL服务端是在4.1版本之后才开始支持预编译的,之后的版本都默认支持预编译,并且预编译还与 MySQL Connector/J(JDBC驱动)的版本有关, Connector/J 5.0.5之前的版本默认支持预编译, Connector/J 5.0.5之后的版本默认不支持预编译, 所以我们用的Connector/J 5.0.5驱动以后版本的话默认都是没有打开预编译的 (如果需要打开预编译,需要配置 useServerPrepStmts 参数)

因为我的测试环境为5.1.47,所以目前版本的预编译默认是关闭的
请添加图片描述

所以我们运行代码,在初始状态下的mysql查询日志是这样的
请添加图片描述

而在数据库链接中加入useServerPrepStmts=true后mysql的查询日志为

请添加图片描述

我们可以看到查询比之前多了一条Prepare数据,表示着预编译开启成功

我们回到代码调试中,当代码执行到PreparedStatement preparedStatement = conn.prepareStatement(sql);时,我们通过断点调试可以进入到ConnectionImpl,javaprepareStatement()方法当中.
请添加图片描述

当我们没有设置useServerPrepStmts=true时,在prepareStatement()方法当中useServerPreparedStmts属性为false,直接跳过当前代码块进入到最后的else代码块
请添加图片描述

请添加图片描述

最后并不会向数据库提交SQL预编译请求

而我们设置useServerPrepStmts=true后,再次调试代码,会发现useServerPreparedStmts属性为true,最后向数据库提交SQL预编译请求
请添加图片描述

请添加图片描述

我们继续调试代码到ResultSet resultSet = preparedStatement.executeQuery(),进入setString()方法

在没有开启预编译的情况下,会进入PreparedStatement.class,在其中的isEscapeNeededForString()方法中对于用户输入的数据中的非法字符进行转义,最后交由数据库端进行运行.

请添加图片描述

而开启了预编译的情况下,会进入ServerPreparedStatement.class,最后数据交由mysql端进行转义处理

在预编译情况下对于order by,like的危害

order by

ORDER BY关键字用于按升序或降序对结果集进行排序。

order by后一般接字段名,而字段名是不能带引号的,比如order by id,如果使用预编译,id在预编译的过程中会被setString()方法自动加引号,而如果带上引号之后就成了order by 'id',现在id就是一个字符串而不是一个字段名了,会产生语法错误.

请添加图片描述

请添加图片描述

可以看到拼接后的sql语句中order by的参数就是字符串,我们在数据库中运行查看
请添加图片描述

请添加图片描述

可以看出经过引号包裹的语句没有起作用

所以在开发过程中,不能参数化的位置,不管怎么拼接,最终都是和使用"+"号拼接字符串的功效一样:拼成了sql语句但没有防sql注入的效果.

我们可以通过构造if语句来对order by以后的语句进行构造进行SQL注入
请添加图片描述

所以需要对order by参数进行特殊的过滤

like

在使用like时,通过平常的sql语句进行构造select * from students where name like '%?%'会报错,所以有时我们看到的代码中会出现拼接形态的like语句,此时就很有可能出现sql注入漏洞
请添加图片描述

正确的like预编译构造方法如下图所示,需要在setString()方法中将%构造出来
请添加图片描述

Mybatis

Mybatis解析执行过程

引用一下先知社区R17a大佬的过程图:
请添加图片描述

以查询SQL分析,主要步骤如下:
1.SqlSession创建过程:SqlSessionFactoryBuilder().build(inputStream)创建一个SqlSession,创建的时候会进行配置文件解析生成Configuration属性实例,解析时会将mapper解析成MapperStatement加到Configuration中,MapperStatement是执行SQL的必要准备,SqlSource是MapperStatement的属性,实例化前会先创建动态和非动态SqlSource即DynamicSqlSourceRawSqlSourceDynamicSqlSource对应解析$以及动态标签如foreach,RawSqlSource创建时解析#并将#{}换成占位符?

2.执行准备过程:DefaultSqlSession.selectOne()执行sql(如果是从接口getMapper方式执行,首先会从MapperProxy动态代理获取DefaultSqlSession执行方法selectxxx|update|delete|insert),首先从Configuration获取MapperStatement,执行executor.query()。executor执行的第一步会先通过MapperStatement.getBoundSql()获取SQL,此时如果MapperStatement.SqlSource是动态即DynamicSqlSource,会先解析其中的动态标签比如${}会换成具体传入的参数值进行拼接,获取到SQL之后调用executor.doQuery(),如果存在预编译首先会调用JDBC处理预编译的SQL,最终通过PreparedStatementHandler调用JDBC执行SQL;

3.JDBC执行SQL并返回结果集

调试代码(${}和#{}使用的不同)

在通过mybatis数据操作的过程中,在XMLScriptBuilder.parseScriptNode()处会因为${}#{}使用的不同执行不同的方法

请添加图片描述

在进入parseScriptNode()后,先通过parseDynamicTags()方法中的TextSqlNode.isDynamic()判断是否存在${}标志来区分动态和非动态SqlSource

请添加图片描述

TextSqlNode.isDynamic()首先会通过DynamicCheckerTokenParser()中的GenericTokenParser()创建一个${}标识符解析

请添加图片描述

继续下一步调用GenGenericTokenParser.parse对我们的SQL语句进行校验

${}分析

请添加图片描述

parse()中可以看到如果在我们sql语句中发现了${那么继续执行,如果没有就直接返回,而在继续执行的最后调用了builder.append(this.handler.handleToken(expression.toString())),在handler.handleToken中将isDynamic更改为了true
请添加图片描述

请添加图片描述

isDynamic为true,会实例化一个DynamicSqlSource对象,返回sqlSource
请添加图片描述

#{}分析

从上面关于${}的分析可以知道,如果我们的sql语句构造为#{},那么将在XMLScriptBuilder.parseScriptNode方法中使用RawSqlSource来构造sqlSource

在过程中同样经过GenGenericTokenParser.parse对我们的SQL语句进行校验,在其中将#{}替换成了?
请添加图片描述

请添加图片描述

like

在mybatis中错误的like使用语句为select * from user where name like "%${id}%"

通过构造id = 1%" or 1=1 # 使最后在数据库执行select * from user where name like "%1%" or 1=1 # %"

请添加图片描述

正确的构造方法应该为SELECT * FROM user where name like concat('%',#{name}, '%')

order by

至于oeder by的话,和JDBC中的分析相似,如果order by后面跟的变量的话,应该进行校验和过滤

参考

https://www.liaoxuefeng.com/wiki/1252599548343744/1321748435828770
https://juejin.cn/post/6844903490058190862
https://xz.aliyun.com/t/10593
https://xz.aliyun.com/t/10686

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值