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

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

引言

MyBatis 是一種持久層框架,介於 JDBC 和 Hibernate 之間。通過 MyBatis 減少了手寫 SQL 語句的痛苦,使用者可以靈活使用 SQL 語句,支持高級映射。但是 MyBatis 的推出不是隻是為了安全問題,有很多開發認為使用了 MyBatis 就不會存在 SQL 注入了,真的是這樣嗎?

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

技術背景

寫本文的起源主要是來源於內網發現的一次 SQL 注入。我們發現內網的一個請求的 keyword 參數存在 SQL 注入,簡單地介紹一下需求背景。

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

<code>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;
}/<code>

很明顯,需求是希望實現 diaplay_name, org,status 以及 id 的模糊查詢,但開發在這裡自己創建了一個 addKeywordTo 方法,通過這個方法創建了一個涉及多個字段的模糊查詢條件。

有一個有趣的現象,在內網發現的絕大多數 SQL 注入的注入點,基本都是模糊查詢的地方。可能很多開發往往覺得模糊查詢是不是就不會存在 SQL 注入的問題。

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

<code> 
    <where>
      <foreach>
        
          <trim>
            <foreach>
              <choose>
                <when>
                  and ${criterion.condition}
                /<when>
                <when>
                  and ${criterion.condition} #{criterion.value}

                /<when>
                <when>
                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                /<when>
                <when>
                  and ${criterion.condition}
                  <foreach>
                    #{listItem}
                  /<foreach>
                /<when>
              /<choose>
            /<foreach>
          /<trim>
        

      /<foreach>
    /<where>
  
/<code>
<code>    <select>
    select
    
      distinct
    

    <include>
    from user
    
      <include>
    

    
      order by ${orderByClause}
    

  /<select>/<code>

我們再回過頭看一下上面 JAVA 代碼中的 addCriterion 方法,這個方法是通過 MyBatis generator 生成的。

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

}/<code>

這裡的 addCriterion 方法只傳入了一個字符串參數,這裡其實使用了重載,還有其它的 addCriterion 方法傳入的參數個數不同。這裡使用的方法只傳入了一個參數,被理解為 condition,因此只是添加了一個只有 condition 的 Criterion。現在再來看 xml 中的 Example_Where_Clause,在遍歷 criteria 時,由於 criterion 只有 condition 沒有 value,那麼只會進去條件 criterion.noValue,這樣整個 SQL 注入的形成就很清晰了。

<code><when>
    and ${criterion.condition}
/<when>/<code>

正確寫法

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

第一種,我們可以用一種非常簡單直接的方法,在 addKeywordTo 方法裡面 對 keword 進行過濾,這樣其實也可以避免 SQL 注入。通過正則匹配將 keyword 裡面所有非字母或者數字的字符都替換成空字符串,這樣自然也就不可能存在 SQL 注入了。

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

但是這種寫法並不是一種科學的寫法,這樣的寫法存在一種弊端,就是如果你的 keyword 需要包含符號該怎麼辦,那麼你是不是就要考慮更多的情況,是不是就需要添加更多的邏輯判斷,是不是就存在被繞過的可能了?那麼正確的寫法應該是什麼呢?其實 mybatis 官網 已經給出了 Comple Queries 的範例:

<code> 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);/<integer>/<integer>/<code>

上面等同的 SQL 語句是:

<code>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)/<code>

現在讓我們將一開始的 addKeywordTo 方法進行改造:

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

這樣的寫法才是一種比較標準的寫法了。or() 方法會產生一個新的 Criteria 對象,添加到 oredCriteria 中,並返回這個 Criteria 對象,從而可以鏈式表達,為其添加 Criterion。這樣添加的的 Criteria 就是包含 condition 以及 value 的,在做條件查詢的時候,就會進入到 criterion.singleValue 中,那麼 keyword 參數只會傳入到 value 中,而 value 是通過 #{} 傳入的。

<code><when>
  and ${criterion.condition} #{criterion.value}
/<when>/<code>

總結一下,導致這個 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 語句的預編譯。

<code>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;
}/<code>

值得注意的一點是,這裡的 PreparedStatement 嚴格意義上來說並不是完全等同於預編譯。其實預編譯分為客戶端的預編譯以及服務端的預編譯,4.1 之後的 MySql 服務器端已經支持了預編譯功能。

很多主流持久層框架(MyBatis,Hibernate) 其實都沒有真正的用上預編譯,預編譯是要我們自己在參數列表上面配置的,如果我們不手動開啟,JDBC 驅動程序 5.0.5 以後版本 默認預編譯都是關閉的。

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

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

數據庫 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 插件,只要準備好配置文件以及數據庫相關信息,就可以通過這個插件生成相應代碼了。

<code>
 generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorconfiguration>
    <context>
        <commentgenerator>
            <property>
            <property>
        /<commentgenerator>

        
        <jdbcconnection>                        connectionURL="jdbc:mysql://localhost:3306/mybaits_test"
                        userId="xxx"
                        password="xxx">
        /<jdbcconnection>

        <javatyperesolver>
            <property>
        /<javatyperesolver>

        <javamodelgenerator>
            <property>
            <property>
            <property>
        /<javamodelgenerator>

        <sqlmapgenerator>
            <property>
        /<sqlmapgenerator>

        <javaclientgenerator>

            <property>
        /<javaclientgenerator>


        <table>
    /<context>
/<generatorconfiguration>/<code>
聊一聊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。這樣可以更好地直接杜絕風險,因為一旦有風險點,就有發生問題的可能。

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

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


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

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


分享到:


相關文章: