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

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

更多優選php

引言

MyBatis 是一種持久層框架,介於 JDBCHibernate 之間。經過 MyBatis 減小了手寫 SQL 語句的痛苦,使用者能夠靈活使用 SQL 語句,支持高級映射。可是 MyBatis 的推出不是隻是爲了安全問題,有不少開發認爲使用了 MyBatis 就不會存在 SQL 注入了,真的是這樣嗎?java

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

技術背景

寫本文的起源主要是來源於內網發現的一次 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_nameorgstatus 以及 id 的模糊查詢,但開發在這裏本身建立了一個 addKeywordTo 方法,經過這個方法建立了一個涉及多個字段的模糊查詢條件。sql

有一個有趣的現象,在內網發現的絕大多數 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,所以只是添加了一個只有 conditionCriterion。如今再來看 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 注入問題。

在這裏插入圖片描述

使用 #{} 能夠避免 SQL 注入嗎?

若是你猛地一看到這個問題,你可能會以爲遲疑?使用 #{} 就能夠完全杜絕 SQL 注入麼,不必定吧。但若是你仔細分析一下,你就會發現答案是確定的。具體的緣由讓我和你娓娓道來。

首先咱們須要先搞清楚 MyBatis 中 #{} 是如何聲明的。當參數經過 #{} 聲明的,參數就會經過 PreparedStatement 來執行,即預編譯的方式來執行。預編譯你應該不陌生,由於在 JDBC 中就已經有了預編譯的接口。

這也對應了開頭文中咱們提到的一點,Mybatis 並非能解決 SQL 注入的核心,預編譯纔是。預編譯不只能夠對 SQL 語句進行轉義,避免 SQL 注入,還能夠增長執行效率。Mybatis 底層其實也是經過 JDBC 來實現的。以 MyBatis 3.3.1 爲例,jdbc 中的 SqlRunner 就設計到具體 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 服務器端已經支持了預編譯功能。

不少主流持久層框架(MyBatisHibernate) 其實都沒有真正的用上預編譯,預編譯是要咱們本身在參數列表上面配置的,若是咱們不手動開啓,JDBC 驅動程序 5.0.5 之後版本 默認預編譯都是關閉的。

須要經過配置參數來進行開啓:

jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true
複製代碼

數據庫 SQL 執行包含多個階段以下圖所示,但咱們這裏針對於 SQL 語句客戶端的預編譯在發送到服務端以前就已經完成了。在服務器端主要考慮的就是性能問題,這不是本文的重點。

固然,每個數據庫實現的預編譯方式可能都有一些差異。可是對於防止 SQL 注入,在 MyBatis 中只要使用 #{} 就能夠了,由於這樣就會實現 SQL 語句的參數化,避免直接引入惡意的 SQL 語句並執行。

在這裏插入圖片描述

MyBatis generator 的使用

對於使用 MyBatisMyBatis 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>
複製代碼

在這裏插入圖片描述

在這裏我想強調的是一個關鍵參數的配置,即 targetRuntime 參數。這個參數有2種配置項,即 MyBatis3MyBatis3Simple,MyBatis3 爲默認配置項。MyBatis3Simple 只會生成基本的增刪改查,而 MyBatis3 會生成帶條件的增刪改查,全部的條件都在 XXXexample 中封裝。

使用 MyBatis3 時,enableSelectByExampleenableDeleteByExampleenableCountByExample 以及 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方面的架構、面試資料(微服務、集羣、分佈式、中間件等),有須要的小夥伴能夠關注公衆號【程序員內點事】,無套路自行領取

相關文章
相關標籤/搜索