天天看點

聊一聊MyBatis 和 SQL 注入間的恩恩怨怨

引言

MyBatis

是一種持久層架構,介于

JDBC

Hibernate

之間。通過 MyBatis 減少了手寫 SQL 語句的痛苦,使用者可以靈活使用 SQL 語句,支援進階映射。但是 MyBatis 的推出不是隻是為了安全問題,有很多開發認為使用了 MyBatis 就不會存在 SQL 注入了,真的是這樣嗎?

使用了 MyBatis 就不會有 SQL 注入了嗎? 答案很明顯是 NO。 MyBatis它隻是一種持久層架構,它并不會為你解決安全問題。當然,如果你能夠遵循規範,按照架構推薦的方法開發,自然也就避免 SQL 注入問題了。本文就将 MyBatis 和 SQL 注入這些恩恩怨怨掰扯掰扯。(注本文所說的 MyBatis 預設指的是 Mybatis3)

技術背景

寫本文的起源主要是來源于内網發現的一次 SQL 注入。我們發現内網的一個請求的

keyword

參數存在 SQL 注入,簡單地介紹一下需求背景。

基本上這個接口就是實作多個字段可以實作 keyword 的模糊查詢,這應該是一個比較常見的需求。隻不過這裡存在多個查詢條件。經過一番搜尋,我們發現問題的核心處于以下代碼:

public Criteria addKeywordTo(String keyword) {
  StringBuilder sb = new StringBuilder();
  sb.append("(display_name like '%" + keyword + "%' or ");
  sb.append("org like '" + keyword + "%' or ");
  sb.append("status like '%" + keyword + "%' or ");
  sb.append("id like '" + keyword + "%') ");
  addCriterion(sb.toString());
  return (Criteria) this;
}           

複制

很明顯,需求是希望實作

diaplay_name

org

status

以及

id

的模糊查詢,但開發在這裡自己建立了一個

addKeywordTo

方法,通過這個方法建立了一個涉及多個字段的模糊查詢條件。

有一個有趣的現象,在内網發現的絕大多數 SQL 注入的注入點,基本都是

模糊查詢

的地方。可能很多開發往往覺得模糊查詢是不是就不會存在 SQL 注入的問題。

分析一下這個開發為什麼會這麼寫,在他沒有意識到這樣的寫法存在 SQL 注入問題的時候,這樣的寫法他可能認為是最省事的,到時直接把查詢條件拼進去就可以了。以上代碼是問題的核心,我們再看一下對應的 xml 檔案:

<sql id="Example_Where_Clause" >
    <where >
      <foreach collection="oredCriteria" item="criteria" separator="or" >
        <if test="criteria.valid" >
          <trim prefix="(" suffix=")" prefixOverrides="and" >
            <foreach collection="criteria.criteria" item="criterion" >
              <choose >
                <when test="criterion.noValue" >
                  and ${criterion.condition}
                </when>
                <when test="criterion.singleValue" >
                  and ${criterion.condition} #{criterion.value}
                </when>
                <when test="criterion.betweenValue" >
                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                </when>
                <when test="criterion.listValue" >
                  and ${criterion.condition}
                  <foreach collection="criterion.value" item="listItem" open="(" close=")" separator="," >
                    #{listItem}
                  </foreach>
                </when>
              </choose>
            </foreach>
          </trim>
        </if>
      </foreach>
    </where>
  </sql>           

複制

<select id="selectByExample" resultMap="BaseResultMap" parameterType="com.doctor.mybatisdemo.domain.userExample" >
    select
    <if test="distinct" >
      distinct
    </if>
    <include refid="Base_Column_List" />
    from user
    <if test="_parameter != null" >
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null" >
      order by ${orderByClause}
    </if>
  </select>           

複制

我們再回過頭看一下上面

JAVA

代碼中的

addCriterion

方法,這個方法是通過

MyBatis generator

生成的。

protected void addCriterion(String condition) {
    if (condition == null) {
        throw new RuntimeException("Value for condition cannot be null");
    }
    criteria.add(new Criterion(condition));
}           

複制

這裡的

addCriterion

方法隻傳入了一個字元串參數,這裡其實使用了重載,還有其它的

addCriterion

方法傳入的參數個數不同。這裡使用的方法隻傳入了一個參數,被了解為

condition

,是以隻是添加了一個隻有

condition

Criterion

。現在再來看 xml 中的

Example_Where_Clause

,在周遊

criteria

時,由于 criterion 隻有 condition 沒有 value,那麼隻會進去條件

criterion.noValue

,這樣整個 SQL 注入的形成就很清晰了。

<when test="criterion.noValue" >
    and ${criterion.condition}
</when>           

複制

正确寫法

既然上面的寫法不正确,那正确的寫法應該是什麼呢?

第一種,我們可以用一種非常簡單直接的方法,在

addKeywordTo

方法裡面 對

keword

進行過濾,這樣其實也可以避免 SQL 注入。通過正則比對将

keyword

裡面所有非字母或者數字的字元都替換成空字元串,這樣自然也就不可能存在 SQL 注入了。

keyword = keyword.replaceAll("[^a-zA-Z0-9\s+]", "");           

複制

但是這種寫法并不是一種科學的寫法,這樣的寫法存在一種弊端,就是如果你的

keyword

需要包含符号該怎麼辦,那麼你是不是就要考慮更多的情況,是不是就需要添加更多的邏輯判斷,是不是就存在被繞過的可能了?那麼正确的寫法應該是什麼呢?其實

mybatis 官網

已經給出了

Comple Queries

的範例:

TestTableExample example = new TestTableExample();

  example.or()
    .andField1EqualTo(5)
    .andField2IsNull();

  example.or()
    .andField3NotEqualTo(9)
    .andField4IsNotNull();

  List<Integer> field5Values = new ArrayList<Integer>();
  field5Values.add(8);
  field5Values.add(11);
  field5Values.add(14);
  field5Values.add(22);

  example.or()
    .andField5In(field5Values);

  example.or()
    .andField6Between(3, 7);           

複制

上面等同的 SQL 語句是:

where (field1 = 5 and field2 is null)
     or (field3 <> 9 and field4 is not null)
     or (field5 in (8, 11, 14, 22))
     or (field6 between 3 and 7)           

複制

現在讓我們将一開始的

addKeywordTo

方法進行改造:

public void addKeywordTo(String keyword, UserExample userExample) {
  userExample.or().andDisplayNameLike("%" + keyword + "%");
  userExample.or().andOrgLike(keyword + "%");
  userExample.or().andStatusLike("%" + keyword + "%");
  userExample.or().andIdLike(keyword + "%");
}           

複制

這樣的寫法才是一種比較标準的寫法了。

or()

方法會産生一個新的

Criteria

對象,添加到

oredCriteria

中,并傳回這個

Criteria

對象,進而可以鍊式表達,為其添加

Criterion

。這樣添加的的

Criteria

就是包含

condition

以及

value

的,在做條件查詢的時候,就會進入到

criterion.singleValue

中,那麼 keyword 參數隻會傳入到

value

中,而

value

是通過

#{}

傳入的。

<when test="criterion.singleValue" >
  and ${criterion.condition} #{criterion.value}
</when>           

複制

總結一下,導緻這個 SQL 注入的原因還是開發沒有按照規範來寫,自己造輪子寫了一個方法來進行模糊查詢,殊不知帶來了 SQL 注入漏洞。其實,

Mybatis generator

已經為每個字段生成了豐富的方法,隻要合理使用,就一定可以避免 SQL 注入問題。

聊一聊MyBatis 和 SQL 注入間的恩恩怨怨

在這裡插入圖檔描述

使用 #{} 可以避免 SQL 注入嗎?

如果你猛地一看到這個問題,你可能會覺得遲疑?使用

#{}

就可以徹底杜絕 SQL 注入麼,不一定吧。但如果你仔細分析一下,你就會發現答案是肯定的。具體的原因讓我和你娓娓道來。

首先我們需要先搞清楚 MyBatis 中

#{}

是如何聲明的。當參數通過

#{}

聲明的,參數就會通過

PreparedStatement

來執行,即預編譯的方式來執行。預編譯你應該不陌生,因為在

JDBC

中就已經有了預編譯的接口。

這也對應了開頭文中我們提到的一點,Mybatis 并不是能解決 SQL 注入的核心,預編譯才是。預編譯不僅可以對 SQL 語句進行轉義,避免 SQL 注入,還可以增加執行效率。Mybatis 底層其實也是通過 JDBC 來實作的。以 MyBatis 3.3.1 為例,jdbc 中的 SqlRunner 就設計到具體 SQL 語句的實作。

聊一聊MyBatis 和 SQL 注入間的恩恩怨怨

在這裡插入圖檔描述

以 update 方法為例,可以看到就是通過 JAVA 中

PreparedStatement

來實作 sql 語句的預編譯。

public int update(String sql, Object... args) throws SQLException {
    PreparedStatement ps = this.connection.prepareStatement(sql);

    int var4;
    try {
        this.setParameters(ps, args);
        var4 = ps.executeUpdate();
    } finally {
        try {
            ps.close();
        } catch (SQLException var11) {
            ;
        }

    }

    return var4;
}           

複制

值得注意的一點是,這裡的

PreparedStatement

嚴格意義上來說并不是完全等同于預編譯。其實預編譯分為用戶端的預編譯以及服務端的預編譯,4.1 之後的 MySql 伺服器端已經支援了預編譯功能。

很多主流

持久層架構

(

MyBatis

Hibernate

) 其實都沒有真正的用上預編譯,預編譯是要我們自己在參數清單上面配置的,如果我們不手動開啟,JDBC 驅動程式 5.0.5 以後版本 預設預編譯都是關閉的。

需要通過配置參數來進行開啟:

jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true           

複制

資料庫 SQL 執行包含多個階段如下圖所示,但我們這裡針對于 SQL 語句用戶端的預編譯在發送到服務端之前就已經完成了。在伺服器端主要考慮的就是性能問題,這不是本文的重點。

當然,每一個資料庫實作的預編譯方式可能都有一些差别。但是對于防止 SQL 注入,在 MyBatis 中隻要使用

#{}

就可以了,因為這樣就會實作 SQL 語句的參數化,避免直接引入惡意的 SQL 語句并執行。

聊一聊MyBatis 和 SQL 注入間的恩恩怨怨

在這裡插入圖檔描述

MyBatis generator 的使用

對于使用

MyBatis

MyBatis generator

肯定是必不可少的使用工具。MyBatis 是針對 MyBatis 以及 iBATIS 的代碼生成工具,支援 MyBatis 的所有版本以及 iBATIS 2.2.0 版本以上。

因為在現實的業務開發中,肯定會涉及到很多表,開發不可能自己一個去手寫相應的檔案。通過 MyBatis generator 就可以生成相應的

POJO 檔案

SQL Map XML

檔案以及可選的 JAVA 用戶端代碼。

常用的使用 MyBatis generator 的方式是直接通過使用 Maven 的

mybatis-generator-maven-plugin

插件,隻要準備好配置檔案以及資料庫相關資訊,就可以通過這個插件生成相應代碼了。

<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <context id="MysqlTables" targetRuntime="MyBatis3">
        <commentGenerator>
            <property name="suppressAllComments" value="false" />
            <property name="suppressDate" value="false" />
        </commentGenerator>

        <!-- 資料庫連結URL、使用者名、密碼 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/mybaits_test"
                        userId="xxx"
                        password="xxx">
        </jdbcConnection>

        <javaTypeResolver>
            <property name="forceBigDecimals" value="true" />
        </javaTypeResolver>

        <javaModelGenerator targetPackage="com.doctor.mybatisdemo.domain" targetProject="src/main/java/">
            <property name="constructorBased" value="false" />
            <property name="enableSubPackages" value="false" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <sqlMapGenerator targetPackage="myBatisGeneratorDemoConfig" targetProject="src/main/resources">
            <property name="enableSubPackages" value="false" />
        </sqlMapGenerator>

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.doctor.mybatisdemo.dao" targetProject="src/main/java/">
            <property name="enableSubPackages" value="false" />
        </javaClientGenerator>

<!--         要生成那些表(更改tableName和domainObjectName就可以) -->
        <table tableName="user" domainObjectName="user"/>
    </context>
</generatorConfiguration>           

複制

聊一聊MyBatis 和 SQL 注入間的恩恩怨怨

在這裡插入圖檔描述

在這裡我想強調的是一個關鍵參數的配置,即

targetRuntime

參數。這個參數有2種配置項,即

MyBatis3

MyBatis3Simple

,MyBatis3 為預設配置項。

MyBatis3Simple

隻會生成基本的增删改查,而

MyBatis3

會生成帶條件的增删改查,所有的條件都在 XXXexample 中封裝。

使用 MyBatis3 時,

enableSelectByExample

enableDeleteByExample

enableCountByExample

以及

enableUpdateByExample

這些屬性為 true,就會生成相應的動态語句。這也就是我們上述 Example_Where_Clause 生成的原因。

如果使用配置項 MyBatis3Simple,那麼生成的 SQL Map XML 檔案将非常簡單,隻包含一些基本的方法,也不會産生上面的動态方法。可以這麼說,如果你使用 MyBatis3Simple 話,并且不額外改造,因為裡面所有的變量都是通過

#{}

引入,就不可能會有 SQL 注入的問題。

但是現實業務中往往涉及到複雜的查詢條件,而且一般開發使用的都是祖傳配置檔案,是以到底是使用 MyBatis3 還是 MyBatis3Simple,還是需要具體問題,具體看待。不過如果你是使用預設配置,你就需要當心了,謹記一點,外部傳入的參數是極有可能是不安全的,是不可以直接引入處理的。意思到這一點,就基本可以很好地避免 SQL 注入問題了。

總結

這篇文章從内網的一個 SQL 注入漏洞引發的對 MyBatis 的使用問題思考,對 MyBatis 中

#{}

工作的原理以及

Mybatis generator

的使用多個方面做了進一步的思考。

可以總結以下幾點:

  • 能不使用拼接就不要使用拼接,這應該也是避免

    SQL 注入

    最基本的原則
  • 在使用

    ${}

    傳入變量的時候,一定要注意變量的引入和過濾,避免直接通過 ${} 傳入外部變量
  • 不要自己

    造輪子

    ,尤其是在安全方面,其實在這個問題上,架構已經提供了标準的方法。如果按照規範開發的話,也不會導緻 SQL 注入問題
  • 可以注意 MyBatis 中

    targetRuntime

    的配置,如果不需要複雜的條件查詢的話,建議直接使用

    MyBatis3Simple

    。這樣可以更好地直接杜絕風險,因為一旦有風險點,就有發生問題的可能。
作者:madneal@平安銀行應用安全團隊 ,檢視原文

今天就說這麼多,如果本文對您有一點幫助,希望能得到您一個點贊👍哦

您的認可才是我寫作的動力!

整理了一些Java方面的架構、面試資料(微服務、叢集、分布式、中間件等),有需要的小夥伴可以關注公衆号【程式員内點事】,無套路自行領取