mybatis中如何防止sql注入和传参

2022-10-11 22:02:48 110 0
魁首哥

环境

使用my SQL ,数据库名为test,含有1表名为users,users内数据如下

JDBC 下的SQL注入

在JDBC下有两种方法执行 SQL语句 ,分别是 Statement 和PrepareStatement,即其中,PrepareStatement为预编译

Statement

SQL语句

 SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'
  

当传入数据为

 username = admin
password = admin
SELECT * FROM users WHERE username = 'admin' AND password = 'admin';
  

即当存在username=admin和password=admin的数据时则返回此用户的数据

万能密码:admin’ and 1=1#

最终的sql语句变为了

 SELECT \* FROM users WHERE username = \'admin\' and 1=1#
  

即返回用户名为admin,同时1=1的所有数据,1=1恒为真,所以始终返回所有数据

如果输入的是:admin’ or 1=1#就会返回所有数据,因为admin’ or 1=1恒为真

所以JDBC使用Statement是不安全的,需要程序员做好过滤,所以一般使用JDBC的程序员会更喜欢使用PrepareStatement做预编译,预编译不仅提高了程序执行的效率,还提高了安全性

PreParedStatement

与Statement的区别在于PrepareStatement会对SQL语句进行预编译,预编译的好处不仅在于在一定程度上防止了 sql注入 ,还减少了sql语句的编译次数,提高了性能,其原理是先去编译sql语句,无论最后输入为何,预编译的语句只是作为 字符串 来执行,而SQL注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对SQL语句进行解析,因此解决了注入问题

因为SQL语句编译阶段是进行词法分析、语法分析、语义分析等过程的,也就是说编译过程识别了关键字、执行逻辑之类的东西,编译结束了这条SQL语句能干什么就定了。而在编译之后加入注入的部分,就已经没办法改变执行逻辑了,这部分就只能是相当于输入字符串被处理

而Statement方法在每次执行时都需要编译,会增大系统开销。理论上PrepareStatement的效率和安全性会比Statement要好,但并不意味着使用PrepareStatement就绝对安全,不会产生SQL注入。

PrepareStatement防御预编译的写法是使用?作为占位符然后将SQL语句进行预编译,由于?作为 占位符 已经告诉数据库整个SQL语句的结构,即?处传入的是参数,而不会是sql语句,所以即使攻击者传入sql语句也不会被数据库解析

 String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
  

//预编译sql语句

 PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setString(1,username);
pstt.setString(2, password);
  

ResultSet resultSet = pstt.executeQuery();//返回结果集,封装了全部地产部的查询结果

首先先规定好SQL语句的结构,然后在对占位符进行数据的插入,这样就会对sql语句进行防御,攻击者构造的paylaod会被解释成普通的字符串,我们可以通过过输出查看最终会变成什么sql语句

可以发现还会对 单引号 进行转义,一般只能通过宽字节注入,下面将会在代码的层面展示为什么预编译能够防止SQL注入,同时解释为什么会多出一个转义符

不安全的预编译

拼接

众所周知,sql注入之所以能被攻击者利用,主要原因在于攻击者可以构造payload,虽然有的开发人员采用了预编译但是却由于缺乏安全思想或者是偷懒会直接采取拼接的方式构造SQL语句,此时进行预编译则无法阻止SQL注入的产生

代码(稍稍替换一下上面的代码):

 //创建sql语句
String sql = "SELECT * FROM users WHERE username = '" + req.getParameter("username") + "' AND password = '" + req.getParameter("password") + "'";
System.out.println(sql);
//预编译sql语句
PreparedStatement pstt = connection.prepareStatement(sql);
ResultSet resultSet = pstt.executeQuery(sql);//返回结果集,封装了全部的产部的查询结果
  

这样即使使用了预编译,但是预编译的语句已经是被攻击者构造好的语句,所以无法防御SQL注入

又或者是前面使用?占位符后,又对语句进行拼接,也会导致SQL注入

想要做到阻止sql注入,首先要做到使用?作为占位符,规定好sql语句的结构,然后在后面不破坏结构

使用in语句

String sql = \”delete from users where id in(\”+delIds+\”);

此删除语句大多用在复选框内,在in当中使用拼接而不使用占位符做预编译的原因是因为很多时候无法确定deIds里含有多少个对象

输入: 1,2

正常只会输出id为1和2的值

如果此时输入: 1,2) or 1=1#

就会形成SQL注入,输出苦库里所有的值

正确写法:

还是要用到预编译,所以我们要对传入的对象进行处理,首先确定对象的个数,然后增加同量的占位符?以便预编译

 public int gradeDelete(Connection con, String delIds) throws  Exception {
    String num = "";
    //将对象分割开来,分割的点以实际而定
    String[] spl = delIds.split(".");

    //根据对象的个数添加同量的占位符?,用来预编译
    for(int i = 0; i< spl.length; i++){
        if(i == 0){
            num += "?";
        } else {
            num += ".?";
        }
    }
    String sql = "delete from users where id in("+num+")";
    prepareStatement pstmt = con.prepareStatement(sql);
    try {
        for(int j = 0; j < spl.length; j++){
            pstmt.setInt(j+1,  integer .parseint(spl[j]));
        }
        return pstmt.executeUpdate();
    } catch(Exception e){
        //
    }

    return 0;
}
  

bilibili 的删除视频为例,当我取消收藏夹复数个视频的收藏时抓到的包为

 892223071%3A2%2C542789708%3A2%2C507228005%3A2%2C422244777%3A2%2C549672309%3A2%2C719381183%3A2%2C976919238%3A2%2C722053417%3A2
解码后为
892223071:2,542789708:2,507228005:2,422244777:2,549672309:2,719381183:2,976919238:2,722053417:2
  

可以发现其以:2,分割,那我们只需在 split 中填写

String\[\] spl = delIds.split(\”:2,\”);

即可,结果为:

然后再使用预编译

使用like语句

  boolean  jud = true;
String sql = "select * from users ";
System.out.println("请输入要查询的内容:");
String con = sc.nextLine();
for (int i = 0; i < con.length(); i++){
     if (!Character.isDigit(con.charAt(i))){
        jud = false;
        break;
    }
}
if(jud){
    sql += "where password like '%" + con + "%'";
}else{
    sql += "where username like '%" + con + "%'";
}
  

当用户输入的为字符串则查询用户名和密码含有输入内容的用户信息,当用户输入的为纯数字则单查询密码,用拼接地方式会造成SQL注入

正常执行:

SQL注入

正确写法

首先我们要将拼接的地方全部改为?做占位符,但是使用占位符后要使用setString来把传入的参数替换占位符,所以我们要先进行判断,判断需要插入替换多少个占位符

 boolean jud = true;
int v = 0;
String sql = "select * from users ";
System.out.println("请输入要查询的内容:");
String con = sc.nextLine();
for (int i = 0; i < con.length(); i++){
    if(!Character.isDigit(con.charAt(i))){
        jud = false;
        break;
    }
}
if(jud){
    sql += "where password like ?";
    v = 1;
}else{
    sql += "where username like ? and password like ?";
    v = 2;
}

//预编译sql语句
PreparedStatement pstt = connection.prepareStatement(sql);
if(v == 1){
    pstt.setString(1, "%"+con+"%");
}else if (v == 2){
    pstt.setString(1, "%"+con+"%");
    pstt.setString(2, "%"+con+"%");
}
  

尝试进行SQL注入

发现被转移了

使用 order by 语句

通过上面对使用in关键字和like关键字发现,只需要对要传参的位置使用占位符进行预编译时似乎就可以完全防止SQL注入,然而事实并非如此,当使用order by语句时是无法使用预编译的,原因是order by字句后面需要加字段名或者字段位置,而 字段名 是不能带引号的,否则就会被认为是一个字符串而不是字段名,然而使用PreapareStatement将会强制给参数加上’,我在下面会在代码层面分析为什么会这样处理参数

所以,在使用order by语句时就必须得使用拼接的Statement,所以就会造成SQL注入,所以还要在过滤上做好防御的准备

调试分析PrepareStatement防止SQL注入的原理

进入调试,深度查看PrepareStatement预编译是怎么防止sql注入的

用户名输入admin,密码输入admin’,目的是查看预编译如何对一个合理的字符串以及一个不合理的字符串进行处理

由于我们输入的username和password分别是admin和admin’,而其中admin’属于非法值,所以我们只在

pstt.setString(2, password);

打上断点,然后步入setString方法

步过到2275行,这里有一个名为needsQuoted的布尔变量,默认为true

然后进入if判断,其中有一个方法为isEscapeNeededForString

步入后发现有一个布尔的needsHexEscape,默认为false,然后将字符串,也就是传入的参数admin’进行逐字解析,判断是否有非法字符,如果有则置needsHexEscape为true且break循环,然后返回needsHexEscape

由于我们传入的是admin’,带有’单引号,所以在switch的过程中会捕捉到,置needsHexEscape = true后直接break掉循环,然后直接返回needsHexEscape

向上返回到了setString方法,经过if判断后运行if体里面的代码,首先创建了一个 StringBuilder ,长度为传入参数即admin+2,然后分别在参数的开头和结尾添加’

简单来说,此switch体的作用就是对正常的字符不做处理,直接向StringBuilder添加同样的字符,如果非法字符,则添加转移后的非法字符,由于不是直接的替换,而是以添加的方式,简单来说就是完全没有使用到用户传入的的参数,自然就做到了防护

我们传入的为admin’,被switch捕捉到后’后会在StringBuilder添加\和’,最终我们的admin’会变为’admin\’,也就是’admin’,同样防止了SQL注入最重要的一环–闭合语句

然后根据要插入占位符的位置进行插入

mybatis 下的SQL注入

Mybatis的两种传参方式

首先要了解在Mybatis下有两种传参方式,分别是KaTeX parse error: Expected ‘EOF’, got ‘#’ at position 5: {}以及#̲{},其区别是,使用{}的方式传参,mybatis是将传入的参数直接拼接到SQL语句上,二使用#{}传参则是和JDBC一样转换为占位符来进行预编译

在#{}下运行的结果:

select * from users where username = #{username} and password = #{password}

在${}下运行的结果:

select * from users where username = “${username}” and password = “${password}”

SQL注入

${}

PeopleMapper设置

 
  

正常运行:

 username:admin
password:admin
  

sql注入:

 username:admin" and 1=1#
password:sef
  

成功sql注入

#{}

Mapper设置

 
  

正常运行

 username:admin
password:admin
  

尝试SQL注入

 username:admin" and 1=1#
password:sef
  

SQL注入失败

使用like语句

正确写法

 mysql:
    select * from users where username like concat('%',#{username},'%')
 oracle :
    select * from users where username like '%'||#{username}||'%'
 sqlserver :
    select * from users where username like '%'+#{username}+'%'
  

使用in语句

正确写法

 mysql:
    select * from users where username like concat('%',#{username},'%')
oracle:
    select * from users where username like '%'||#{username}||'%'
sqlserver:
    select * from users where username like '%'+#{username}+'%'
  

使用order by语句

和JDBC同理,使用#{}方式传参会导致order by语句失效,所以使用order by语句的时候还是需要做好过滤

调试分析Mybatis防止SQL注入的原理

本人学艺不精,一直定位定位不到XMLScriptBuilder上,所以只好看一下别人写的mybatis解析过程,通过解析过程来定位方法位置

先说结论,首先Mybatis会先对mapper里面的SQL语句进行判断,判断内容为是以${}传参还是以#{}传参,如果以#{}传参则使用?作为占位符进行预编译,Mybatis只会对SQL语句的占位符做一定的处理,处理传入参数最后的步骤还是调用会JDBC的预编译

完整调用流程:

${}解析执行过程

首先在XMLScriptBuilder中的parseDynamicNode()

在这里进行了一次判断,先说结论,这个isDynamic的判断其实就是判断mapper.xml中的sql语句是使用#{}预编译还是使{}拼接,使用拼接,使用{}则进入DynamicSqlSource,否则进入RawSqlSource

进入parseDynamicTags方法,可以发现有两种情况会使isDynamic为true,而其中isDynamic()就是用来判定的

进入isDynamic()

可以看到运行了两个方法,分别是DynamicCheckerTokenParser()以及createParser(),主要出在createParser()上,查看方法体

发现调用了GenericTokenParser方法,传入了openToken、 close Token以及handler三个参数,其中openToken的值为{、closeToken的值为},很明显就是对sql语句进行解析,判断是否为、 closeToken 的值为,很明显就是对 sql 语句进行解析,判断是否为{}方式传参,进入到GenericTokenParser方法

然而只是单纯的设置变量的值,一直向上返回到isDynamic(),进入下一条语句parser.parse(this.text);

在调试使就可清楚看到传入的值了,${}和sql语句同时出现,猜测就是在这里进行了匹配

进入到parse方法,此方法对sql语句进行解析,当遇到${}的字段则将此位置空(null),从返回的StringBuilder值可以看出

执行完后返回到isDynamic()方法下,在return值递归,其实就是返回isDynamic的值,然后向上返回到一直返回到了parseScriptNode()方法

最终结果就会创建一个DynamicSqlSource对象

至此,对SQL语句的解析告一段落,直到运行到peopleMapper.getPeopleList1(people1),步入到invoke方法

前面的方法大致就是获取传入的参数和获取sql语句,步进到execute方法,此方法作用是判断SQL语句的类型

由于我们的SQL语句使select,所以会落在witch体的select内,步入case select的excuteForMany方法

继续步入selectList方法,后面这里我不知道准确流程是怎么样的,反正经过我一番调试最终到了query方法这里,然后步入getBoundSql方法

步入getBoundSql方法后可以看一下参数,发现sqlSource的类型正是前面设置的DynamicSqlSource

继续步入getBoundSql方法,然后步进到 root SqlNode.apply方法

这里有有个坑点啊,可能是因为我技术不够,由于这个apply方法有很多个实现,直接步进会跑到MixerSqlNode里面,但我查阅了资料发现实际上是在TextSqlNode里面

步入createParser方法,发现调用了GenericTokenParser,这在上面解析的过程也是一样的

从parse方法中返回的StringBuider可以发现,已经成功将参数和SQL语句拼接在一起了

#{}解析执行过程

在前面分析${}的过程中就提到了在XMLScriptBuilder中的parseDynamicNode()方法,目的就是为了判断mapper.xml文件内的SQL语句究竟是用${}方式传参还是使用#{}方式传参,如果是#{}方式则最终会调用RawSqlSource方法

步入RawSqlSource方法

继续运行,步入到sqlSourceParser.parse方法

可以发现出现了解析${}时用到的函数

GenericTokenParser parser = new GenericTokenParser(“#{“, “}”, handler );

进入方法体后发现目的是设置openToken和closeToken的值分别为#{和}

真正对SQL语句进行了操作的是

String sql = parser.parse(originalSql);

步入parser.parse方法,运行到结尾后查看StringBuilder的值,发现函数把#{}用?替换了

到此,解析过程结束,一直运行到peopleMapper.getPeopleList1(people1),步入到invoke方法,然后前面的流程大致和${}解析一致,进入mapperMethod.execute方法,然后会判断执行的sql语句类型,然后进入executeForMany方法,一直运行到selectList方法,最后进入query方法

query方法会调用自身作为返回值

在此方法的返回值又会调用 delegate .query方法,而这个方法就是我执行#{}的方法,进入后一直运行到

 else {
    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
  

后进入

进入queryFromDatabase方法后运行到

 try {
    list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
}
  

进入doQuery方法,进入prepareStatement()方法

其中

Connection connection = this.getConnection(statementLog);

是与数据库建立连接的对象

步入parameterize()方法

继续步入,到setParameters方法

setParameters方法的作用,是将SQL语句和传入的参数进行拼接

List parameterMappings = this.boundSql.getParameterMappings(); 中,获取了boundSql,即获取到了设置的sql语句

ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);

获取到了SQL语句中所需要的参数,我的SQL语句为select * from users where
username = #{username} and password = #{password},所以需要username和password两个参数

运行到

步入setParameter方法

在图示处打上断点,步入setNonNullParameter方法

继续在图示处打上断点,步入setParameter方法

继续在图示处打上断点,步入setNonNullParameter方法

虽然方法名是一样的,但是并不是同一个方法,步入setString方法

这里用到了动态代理,最终还是调用回了jdbc的preperStatement,在图示处打上断点并步入

发现这个setString和上文所讲的JDBC的预编译使用一个函数,后面的编译方式与JDBC相同

Hibernate

Hibernate执行语句的两种方法

Hibernate可以使用hql来执行SQL语句,也可以直接执行SQL语句,无论是哪种方式都有可能导致SQL注入

Hibernate下的SQL注入

HQL

hql语句:

 String hql = "from People where username = '" + username + "' and password = '" + password + "'";
  

首先先观察一下正常登录和错误登陆下的的情况

正常登录:

 Hibernate: 
    /* 
from
    People 
where
    username = 'admin' 
    and password = 'admin' */ select
        people0_.id as id1_0_,
        people0_.username as username2_0_,
        people0_.password as password3_0_ 
    from
        users people0_ 
    where
        people0_.username='admin' 
        and people0_.password='admin'
admin
  

错误登陆:

 Hibernate: 
    /* 
from
    People 
where
    username = 'admin' 
    and password = 'adadawd' */ select
        people0_.id as id1_0_,
        people0_.username as username2_0_,
        people0_.password as password3_0_ 
    from
        users people0_ 
    where
        people0_.username='admin' 
        and people0_.password='adadawd'
  

可以发现之间的区别在于成功登录后最后面返回了用户名

尝试进行SQL注入:

输入:

 请输入用户名:
admin' or '1'='1
请输入密码
qwer
  

返回:

 Hibernate: 
    /* 
from
    People 
where
    username = 'admin' 
    or '1'='1' 
    and password = 'qwer' */ select
        people0_.id as id1_0_,
        people0_.username as username2_0_,
        people0_.password as password3_0_ 
    from
        users people0_ 
    where
        people0_.username='admin' 
        or '1'='1' 
        and people0_.password='qwer'
admin
  

可以发现,经过拼接后,SQL语句变为了

from People where username = ‘admin’ or ‘1’=’1′ and password = ‘qwer’

说明了使用这种拼接的方式和jdbc以及mybatis是一样会产生sql注入的

正确写法:

正确使用以下几种HQL参数绑定的方式可以有效避免注入的产生

位置参数(Positional parameter)

 String parameter = "g1ts";
Query query = session.createQuery("from users name = ?1", User.class);
query.setParameter(1, parameter);
  

命名参数(named parameter)

 Query query = session.createQuery("from users name = ?1", User.class);
String parameter = "g1ts";
Query query = session.createQuery("from users name = :name", User.class);
query.setParameter("name", parameter);
  

命名参数列表(named parameter list)

 List names = Arrays.asList("g1ts", "g2ts");
Query query = session.createQuery("from users where name in (:names)", User.class);
query.setParameter("names", names);
  

类实例(JavaBean)

 user1.setName("g1ts");
Query query = session.createQuery("from users where name =:name", User.class);
query.setProperties(user1);
  

SQL

Hibernate支持使用原生SQL语句执行,所以其风险和JDBC是一致的,直接使用拼接的方法时会导致SQL注入

语句如下:
Query query = session.createNativeQuery(“select * from user where username = ‘” + username + “‘ and password = ‘” + password + “‘”);

正确写法

 String parameter = "g1ts";
Query query = session.createNativeQuery("select * from user where name = :name");
query.setParameter("name",parameter);
  

调试分析Hibernate预防SQL注入原理

Hibernate框架最终还是使用了JDBC中预编译防止SQL注入的方法

完整过程

查看一下hibernate预编译的过程

首先在

List\ list = query.list();

打下断点,步入

步入list方法

继续步入list方法

步入doList方法

步入bind方法

步入nullSafeSet方法

步入getBinder方法

最后调用的st.setString就是jdbc的setString方法

收藏
分享
海报
0 条评论
110
上一篇:大神给你总结的Mysql开发规范与使用技巧 下一篇:SoapClient反序列化SSRF

本站已关闭游客评论,请登录或者注册后再评论吧~

忘记密码?

图形验证码