搜尋此網誌

2010年10月23日 星期六

[技術分享] iBatis/MyBatis 與 SQL Injection

flow前幾天無意間聽到有人提到 iBatis 使用了 PreparedStatement 的機制,所以沒有 SQL Injection 的問題,因此我特地寫了這篇文章來說明 iBatis 與 SQL Injection 的關係。在開始說明之前,我稍微解釋一下 iBatis 這個套件。根據作者的解釋,它是一個 SQL Mapping 的工具,抽象程度高於 JDBC,但是卻低於 ORM (例如 Hibernate)。簡單來說,iBatis 將系統中會使用到的 SQL 指令皆放置在 XML 檔案內,以避免利用程式本身產生與維護 SQL 指令時的困擾。相較於 JDBC,iBatis 也提供了像是 Cache、Transaction Management 等高階的功能,幫助系統開發者簡化資料庫的相關操作。而因為某些原因,iBatis 已經改名為 MyBatis,不過基本上兩者是一樣的。反倒是目前 MyBatis 有 2.x 與 3.x 兩個主要版本,兩者之間卻是不相容的。

回到 SQL Injection 本身,用了 iBatis/MyBatis 就沒有 SQL Injection 的問題了嗎?聰明的讀者,您一定想到了答案。因為如果答案是肯定的,就沒有這篇文章存在的必要性, 所以答案就是即使使用了 iBatis/MyBatis,並不能保證系統就沒有 SQL Injection 的問題。基本上,iBatis/MyBatis 會盡量以PreparedStatement 的方式加以執行,但是卻不是在所有的情況下都是如此的。主要的問題發生於在當使用 inline 方式宣告 SQL 指令的參數時,iBatis/MyBatis 允許兩種參數傳遞的宣告方式,一種是利用 #,另外一種則是透過 $,而後者正是問題所在。$ 表示將參數的內容直接附加於 SQL 指令之內,而不是使用參數化的設定。因此只要使用了 $ 的方式來傳遞參數,就有可能遭受 SQL Injection 的攻擊。

此外,iBatis/MyBatis 也支援所謂 Dynamic SQL 的功能。我們之前提到 Dynamic SQL 正是 SQL Injection 的元兇,但是使用 iBatis/MyBatis 的 Dynamic SQL 卻並不表示就有 SQL Injection 的問題,還是必須取決於參數傳遞的方式。如果使用 # 的方式傳遞參數,即使使用 iBatis/MyBatis 的 Dynamic SQL 依舊是安全的。結論知道了,接下來還是透過簡單的範例程式來看看問題是如何發生的。

首先我在資料庫內鍵入了兩筆虛擬的使用者資料:

mysql> select * from USER_ACCOUNT;
+--------+----------+----------+-----------+
| USERID | username | password | groupname |
+--------+----------+----------+-----------+
|      1 | LMEADORS | PICKLE   | EMPLOYEE  |
|      2 | JDOE     | TEST     | EMPLOYEE  |
+--------+----------+----------+-----------+
2 rows in set (0.00 sec)


 



我以 iBatis/MyBatis 2.x 為例,在設定檔中宣告了兩個功能相同的 SQL 指令,也就是根據使用者帳號與密碼來查詢使用者的資料,通常會用於使用者的登入認證過程。第一個 SQL 指令 (第 3-4 行) 採用 # 的宣告方式,而第二個 SQL 指令 (第 7-8 行) 則採用 $ 的宣告方式。



  1: <select id="checkCredential-1" parameterClass="QueryCondition"
  2:                                resultClass="hashmap">
  3:     SELECT * FROM USER_ACCOUNT WHERE username = #username# 
  4:         AND password = #password#
  5: </select>
  6: <select id="checkCredential-2" parameterClass="QueryCondition"
  7:                                resultClass="hashmap">
  8:     SELECT * FROM USER_ACCOUNT WHERE username = '$username$' 
  9:         AND password = '$password$'
 10: </select>


 



程式針對兩組 SQL 指令分別嘗試 SQL Injection 的攻擊 (第 12-13, 22-23 行)。



  1: String resource = "SqlMapConfig.xml";
  2: Reader reader = Resources.getResourceAsReader(resource);
  3: SqlMapClient sqlMap = 
  4:     SqlMapClientBuilder.buildSqlMapClient(reader);
  5: 
  6: // it's good, and the user is found
  7: QueryCondition qc = new QueryCondition("JDOE", "TEST", null);
  8: Object o = sqlMap.queryForObject("checkCredential-1", qc);
  9: System.out.println("1. " + o);
 10: 
 11: // it's bad, but the user can *NOT* be found (sanitized by iBatis)
 12: qc = new QueryCondition("JDOE' OR 1=1--'", "1234", null);
 13: o = sqlMap.queryForObject("checkCredential-1", qc);
 14: System.out.println("2. " + o);
 15: 		
 16: // it's good, and the user is found
 17: qc = new QueryCondition("JDOE", "TEST", null);
 18: o = sqlMap.queryForObject("checkCredential-2", qc);
 19: System.out.println("3. " + o);
 20: 		
 21: // it's bad, and the user is found (*SQL Injected*)
 22: qc = new QueryCondition("JDOE' OR 1=1--'", "1234", null);
 23: o = sqlMap.queryForObject("checkCredential-2", qc);
 24: System.out.println("4. " + o);


 



我們可以看到第二次的 SQL Injection 攻擊成功 (第 4 行),程式使用的是設定檔中第二個 SQL 指令。針對第一個 SQL 指令的 SQL Injection 並沒有成功 (第 2 行)。



1. {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}
2. null
3. {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}
4. {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}


 



接下來我們來看看 iBatis/MyBatis 的 Dynamic SQL 是如何運作的。第一個 Dynamic SQL 採用 # 的方式加以宣告,而第二個 Dynamic SQL 則採用 $ 的方式加以宣告。



  1: <select id="checkCredentialExtended-1" 
  2:     parameterClass="QueryCondition" resultClass="hashmap">
  3:     SELECT * FROM USER_ACCOUNT
  4:     <dynamic prepend=" WHERE ">
  5:         <isNotEmpty property="username">
  6:             username=#username#
  7:         </isNotEmpty>
  8:     </dynamic>
  9:     <dynamic prepend=" AND ">
 10:         <isNotEmpty property="groupname">
 11:             password=#password#
 12:         </isNotEmpty>
 13:     </dynamic>
 14:     <dynamic prepend=" AND ">
 15:         <isNotEmpty property="groupname">
 16:             groupname=#groupname#
 17:         </isNotEmpty>
 18:     </dynamic>
 19: </select>
 20: <select id="checkCredentialExtended-2" 
 21:     parameterClass="QueryCondition" resultClass="hashmap">
 22:     SELECT * FROM USER_ACCOUNT
 23:     <dynamic prepend=" WHERE ">
 24:         <isNotEmpty property="username">
 25:             username='$username$'
 26:         </isNotEmpty>
 27:     </dynamic>
 28:     <dynamic prepend=" AND ">
 29:         <isNotEmpty property="groupname">
 30:             password='$password$'
 31:         </isNotEmpty>
 32:     </dynamic>
 33:     <dynamic prepend=" AND ">
 34:         <isNotEmpty property="groupname">
 35:             groupname='$groupname$'
 36:         </isNotEmpty>
 37:     </dynamic>
 38: </select>


 



程式同樣針對兩組 SQL 指令分別嘗試 SQL Injection 的攻擊 (第 22-23, 42-43 行)。



  1: String resource = "SqlMapConfig.xml";
  2: Reader reader = Resources.getResourceAsReader(resource);
  3: SqlMapClient sqlMap = 
  4:     SqlMapClientBuilder.buildSqlMapClient(reader);
  5: 
  6: // it's good, and the user is found
  7: QueryCondition qc = new QueryCondition("JDOE", "TEST", null);
  8: Object o = sqlMap.queryForObject("checkCredentialExtended-1", qc);
  9: System.out.println(o);
 10: 
 11: // it's good, and the user is found
 12: qc = new QueryCondition("JDOE", "TEST", "EMPLOYEE");
 13: o = sqlMap.queryForObject("checkCredentialExtended-1", qc);
 14: System.out.println(o);
 15: 
 16: // it's good, but the user is not found (no such user)
 17: qc = new QueryCondition("JDOE", "TEST", "MANAGER");
 18: o = sqlMap.queryForObject("checkCredentialExtended-1", qc);
 19: System.out.println(o);
 20: 
 21: // it's bad, but the user is *NOT* be found (sanitized by iBatis)
 22: qc = new QueryCondition("JDOE' OR 1=1--'", "1234", null);
 23: o = sqlMap.queryForObject("checkCredentialExtended-1", qc);
 24: System.out.println(o);
 25: 
 26: // it's good, and the user is found
 27: qc = new QueryCondition("JDOE", "TEST", null);
 28: o = sqlMap.queryForObject("checkCredentialExtended-2", qc);
 29: System.out.println(o);
 30: 		
 31: // it's good, and the user is found
 32: qc = new QueryCondition("JDOE", "TEST", "EMPLOYEE");
 33: o = sqlMap.queryForObject("checkCredentialExtended-2", qc);
 34: System.out.println(o);
 35: 		
 36: // it's good, but the user is not found (no such user)
 37: qc = new QueryCondition("JDOE", "1234", "MANAGER");
 38: o = sqlMap.queryForObject("checkCredentialExtended-2", qc);
 39: System.out.println(o);
 40: 		
 41: // it's bad, and the user is found (*SQL Injected*)
 42: qc = new QueryCondition("JDOE' OR 1=1--'", "1234", "MANAGER");
 43: o = sqlMap.queryForObject("checkCredentialExtended-2", qc);
 44: System.out.println(o);


 



我們可以看到第二次的 SQL Injection 攻擊成功 (第 8 行),程式使用的是設定檔中第二個 SQL 指令。針對第一個 SQL 指令的 SQL Injection 並沒有成功 (第 4 行)。



  1: {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}
  2: {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}
  3: null
  4: null
  5: {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}
  6: {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}
  7: null
  8: {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}


 



前述是 iBatis/MyBatis 2.x 的例子,而 MyBatis 3.x 雖然與 iBatis/MyBatis 2.x 不相容,但是問題的本質卻是完全一樣的。也就是當透過設定檔指定 SQL 指令時,MyBatis 3.x 一樣有可能遭遇 SQL Injection 的攻擊。除了利用設定檔的方式外,MyBatis 3.x 還可以透過所謂 Mapper class 的機制來指定 SQL 指令,而此一機制一樣有可能遭受 SQL Injection 的攻擊。至於利用 MyBatis 3.x 提供的 SelectBuilder 與 SqlBuilder 所產生的 SQL 指令,因為並不支援以 $ 的方式宣告參數,所以並不會有上述的問題。



因此在使用 iBatis/MyBasit 的 inline 參數宣告方式時,請記得盡量採用 # 的方式加以宣告。如果確有使用 $ 的需求,則需務必做好參數的嚴格檢查,以免產生 SQL Injection 的危害。而 iBatis/MyBatis 除了使用 SQL 指令之外,也可以呼叫預儲程序 (Stored Procedure)。在這種情況下,如何避免 Stored Procedure 遭受 SQL Injection 的攻擊,就成了預儲程序自己的責任了。



沒有留言:

張貼留言

About