前幾天無意間聽到有人提到 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 found7: 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 found17: 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. null3. {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_ACCOUNT4: <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_ACCOUNT23: <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 found7: 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 found12: 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 found27: 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 found32: 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: null4: null5: {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}6: {username=JDOE, groupname=EMPLOYEE, password=TEST, USERID=2}7: null8: {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 的攻擊,就成了預儲程序自己的責任了。
沒有留言:
張貼留言