SQL注入詳解
一、什么是SQL注入
SQL注入(SQL lnjection)是發(fā)生在Web程序中數(shù)據(jù)庫層的安全漏洞,是比較常用的網(wǎng)絡(luò)攻擊方式之一,他不是利用操作系統(tǒng)的BUG來實現(xiàn)攻擊,而是針對程序員編寫時的疏忽,通過SQL語句,實現(xiàn)無賬號登錄,甚至修改數(shù)據(jù)庫。也就是說,SQL注入就是在用戶輸入的字符串中添加SQL語句,如果在設(shè)計不良的程序中忽略了檢查,那么這些注入進去的SQL語句就會被數(shù)據(jù)庫服務(wù)器誤認為是正常的SQL語句而運行,攻擊者就可以執(zhí)行計劃外的命令或者訪問未授權(quán)的數(shù)據(jù)。
二、SQL注入的原理
1.惡意拼接查詢
SQL語句可以對數(shù)據(jù)進行增刪改查,且使用分號來分隔不同命令。例如:
SELECT * FROM user WHERE user_id = $user_id
?其中user_id是傳入的參數(shù),如果傳入?yún)?shù)的值為"1234;DELETE FROM user",那么最終執(zhí)行的查詢?yōu)椋?/p>
SELECT * FROM users WHERE user_id = 1234; DELETE FROM users
如果執(zhí)行以上語句,則會刪除user表中的所有數(shù)據(jù)
2.利用注釋執(zhí)行非法命令
?SQL語句中可以插入注釋,例如
SELECT COUNT(*) AS 'num' FROM score WHERE id=24411 AND version=$version
如果version包含了惡意的字符串" '-1' OR 3 AND SLEEP(500) ",那么最終查詢的語句會變?yōu)?/p>
SELECT COUNT(*) AS 'num' FROM score WHERE id=24411 AND version='-1' OR 3 AND SLEEP(500)
以上惡意查詢只是想耗盡系統(tǒng)資源,SLEEP(500) 將導(dǎo)致 SQL 語句一直運行。如果其中添加了修改、刪除數(shù)據(jù)的惡意指令,那么將會造成更大的破壞。
3.傳入非法參數(shù)
SQL 語句中傳入的字符串參數(shù)是用單引號引起來的,如果字符串本身包含單引號而沒有被處理,那么可能會篡改原本 SQL 語句的作用。 例如:
SELECT * FROM user_name WHERE user_name = $user_name
如果 user_name 傳入?yún)?shù)值為 G'chen,那么最終的查詢語句會變?yōu)椋?/p>
SELECT * FROM user_name WHERE user_name ='G'chen'
一般情況下,以上語句會執(zhí)行出錯,這樣的語句風(fēng)險比較小。雖然沒有語法錯誤,但可能會惡意產(chǎn)生 SQL 語句,并且以一種你不期望的方式運行。
4.添加額外條件
在 SQL 語句中添加一些額外條件,以此來改變執(zhí)行行為。條件一般為真值表達式。例如:
UPDATE users SET userpass='$userpass' WHERE user_id=$user_id;
如果 user_id 被傳入惡意的字符串“1234 OR TRUE”,那么最終的 SQL 語句會變?yōu)椋?/p>
UPDATE users SET userpass= '123456' WHERE user_id=1234 OR TRUE;
如果執(zhí)行以上語句,將更改所有用戶的密碼
三、SQL注入實例

四、如何避免SQL注入
1.過濾輸入內(nèi)容,校驗字符串
過濾輸入內(nèi)容就是在數(shù)據(jù)提交到數(shù)據(jù)庫之前,就把用戶輸入中不合法的字符剔除掉,可以使用編程語言提供的處理函數(shù)或自己的處理函數(shù)來進行過濾,還可以使用正則表達式匹配安全的字符串。
如果值屬于特定的類型或有具體的格式,那么在拼接SQL語句之前就要進行校驗,驗證其有效性,比如對于某個傳入的值,如果可以確定是整形,則要判斷他是否為整型,在瀏覽器端(客戶端)和服務(wù)器端都要進行驗證。
2.參數(shù)化查詢(綁定變量,使用預(yù)編譯查詢)
參數(shù)化查詢目前被視為預(yù)防SQL注入最有效的方法,參數(shù)化查詢是指在設(shè)計與數(shù)據(jù)庫連接并訪問數(shù)據(jù)時,在需要填入數(shù)值或者數(shù)據(jù)的地方,使用參數(shù)(Parameter)來給值
MySQL 的參數(shù)格式是以"?"字符加上參數(shù)名稱而成,如下所示:
UPDATE myTable SET c1 = ?c1, c2 = ?c2, c3 = ?c3 WHERE c4 = ?c4
在使用參數(shù)化查詢的情況下,數(shù)據(jù)庫服務(wù)器不會將參數(shù)的內(nèi)容視為SQL語句的一部分來進行處理,而是在數(shù)據(jù)庫完成SQL語句的編譯之后,才 套用參數(shù)運行,因此就算參數(shù)含有破壞性的指令,也不會被數(shù)據(jù)庫所運行。
使用預(yù)編譯的SQL語句語義不會發(fā)生改變,在SQL語句中,變量用問號?表示,黑客即使本事再大,也無法改變SQL語句的結(jié)構(gòu)
簡單總結(jié),參數(shù)化能防注入的原因在于,語句是語句,參數(shù)是參數(shù),參數(shù)的值并不是語句的一部分,數(shù)據(jù)庫只按語句的語義跑,至于跑的時候是帶一個普通背包還是一個怪物,不會影響行進路線,無非跑的快點與慢點的區(qū)別。
3.安全測試,安全審計
?除了開發(fā)規(guī)范,還需要合適的工具來確保代碼的安全。我們應(yīng)該在開發(fā)過程中應(yīng)對代碼進行審查,在測試環(huán)節(jié)使用工具進行掃描,上線后定期掃描安全漏洞。通過多個環(huán)節(jié)的檢查,一般是可以避免 SQL 注入的。
有些人認為存儲過程可以避免 SQL 注入,存儲過程在傳統(tǒng)行業(yè)里用得比較多,對于權(quán)限的控制是有一定用處的,但如果存儲過程用到了動態(tài)查詢,拼接 SQL,一樣會存在安全隱患。
下面是在開發(fā)過程中可以避免 SQL 注入的一些方法。
?3.1.避免使用動態(tài)SQL
避免將用戶的輸入數(shù)據(jù)直接放在SQL語句中,最好使用準(zhǔn)備好的語句和參數(shù)化查詢,這樣更安全。
3.2.不要將敏感數(shù)據(jù)保留在純文本中
加密存儲在數(shù)據(jù)庫中的私有/機密數(shù)據(jù),這樣可以提供了另一級保護,以防攻擊者成功的排出敏感數(shù)據(jù)
3.3.限制數(shù)據(jù)庫的權(quán)限和特權(quán)
將數(shù)據(jù)庫用戶的功能設(shè)置為最低要求;這將限制攻擊者在設(shè)法獲取訪問權(quán)限時可以執(zhí)行的操作
3.4.避免直接向用戶顯示數(shù)據(jù)庫錯誤
攻擊者可以使用這些錯誤消息來獲取有關(guān)的數(shù)據(jù)庫信息。
四、SQL預(yù)編譯
1.預(yù)編譯語句是什么?
通常我們的一條sql在db接受到最終執(zhí)行完畢返回可以分為下面三個過程:
? ? ? ? 1.詞法和語義的解析
? ? ? ? 2.優(yōu)化sql語句,制定執(zhí)行計劃
? ? ? ? 3.執(zhí)行并返回結(jié)果
這種普通語句被稱為Immediate Statements
但是很多情況,我們的一條sql語句可能會反復(fù)執(zhí)行,或者每次執(zhí)行的時候只有個別的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。
如果每次都需要經(jīng)過上面的詞法語義解析、語句優(yōu)化、制定執(zhí)行計劃等,則效率就明顯不行了。
所謂預(yù)編譯語句就是將這類語句中的值用占位符替代,可以視為將sql語句模板化或者說參數(shù)化,一般稱這類語句叫Prepared Statements或者Parameterized Statements
預(yù)編譯語句的優(yōu)勢在于歸納為:一次編譯、多次運行,省去了解析優(yōu)化等過程;此外預(yù)編譯語句能防止sql注入。
當(dāng)然就優(yōu)化來說,很多時候最優(yōu)的執(zhí)行計劃不是光靠知道sql語句的模板就能決定了,往往就是需要通過具體值來預(yù)估出成本代價。
2.預(yù)編譯語句
(1)建一張測試表t

(2)編譯
通過 PREPARE stmt_name FROM perpare_stm 的語法來預(yù)編譯一條sql語句
PREPARE ?ins FROM 'INSERT INTO t SELECT ?,?';
(3)執(zhí)行
通過 EXECUTE stmt_name [USING @var_name [,@var_name]...]的語法來執(zhí)行預(yù)編譯語句
SET @a=999,@b='hello';
EXECUTE ins USING @a,@b;
此時數(shù)據(jù)已經(jīng)插入。
MySQL中的預(yù)編譯語句作用域是session級,但我們可以通過max_prepare_stmt_count變量來控制全局最大的存儲的預(yù)編譯語句
SET @@global.max_prepared_stmt_count=1;
/*此時設(shè)置預(yù)編譯最大條數(shù)為1,如果繼續(xù)使用預(yù)編譯,就會報錯*/
(4)釋放
如果我們想要釋放一條預(yù)編譯語句,則可以使用{DEALLOCATE | DROP} PREPARE stmt_name的語法進行操作
?DEALLOCATE prepare ins;
五、MyBatis如何預(yù)防SQL注入
觀察兩段代碼區(qū)別:
<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = #{username,jdbcType=VARCHAR}
and password = #{password,jdbcType=VARCHAR}
</select>
?
<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = ${username,jdbcType=VARCHAR}
and password = ${password,jdbcType=VARCHAR}
</select>
mybatis中#和$的區(qū)別:
? ? 1、#將傳入的數(shù)據(jù)都當(dāng)成一個字符串,會對自動傳入的數(shù)據(jù)加一個雙引號。
如:where username=#{username},如果傳入的值是111,那么解析成sql時的值為where username="111", 如果傳入的值是id,則解析成的sql為where username="id".
2、$將傳入的數(shù)據(jù)直接顯示生成在sql中。
如:where username=${username},如果傳入的值是111,那么解析成sql時的值為where username=111;
如果傳入的值是;drop table user;,則解析成的sql為:select id, username, password, role from user where username=;drop table user;
? ? ? ? 3、#能很大程度防止sql注入,$ 方式無法防止sql注入
? ? ? ? 4、$方式一般用于傳入數(shù)據(jù)庫對象,例如傳入表名.
5、一般能用#的就別用$,若不得不使用“${xxx}”這樣的參數(shù),要手工地做好過濾工作,來防止sql注入攻擊。
6、在MyBatis中,“${xxx}”這樣格式的參數(shù)會直接參與SQL編譯,從而不能避免注入攻擊。但涉及到動態(tài)表名和列名時,只能使用“${xxx}”這樣的參數(shù)格式。所以,這樣的參數(shù)需要我們在代碼中手工進行處理來防止注入。
【結(jié)論】在編寫MyBatis的映射語句時,盡量采用“#{xxx}”這樣的格式。若不得不使用“${xxx}”這樣的參數(shù),要手工地做好過濾工作,來防止SQL注入攻擊。
mybatis是如何做到防止SQL注入的
? MyBatis框架作為一款半自動化的持久層框架,其SQL語句都要我們自己手動編寫,這個時候當(dāng)然需要防止SQL注入。其實,MyBatis的SQL是一個具有“輸入+輸出”的功能,類似于函數(shù)的結(jié)構(gòu),參考上面的兩個例子。其中,parameterType表示了輸入的參數(shù)類型,resultType表示了輸出的參數(shù)類型?;貞?yīng)上文,如果我們想防止SQL注入,理所當(dāng)然地要在輸入?yún)?shù)上下功夫。上面代碼中使用#的即輸入?yún)?shù)在SQL中拼接的部分,傳入?yún)?shù)后,打印出執(zhí)行的SQL語句,會看到SQL是這樣的:
select id, username, password, role from user where username=? and password=?
不管輸入什么參數(shù),打印出的SQL都是這樣的。這是因為MyBatis啟用了預(yù)編譯功能,在SQL執(zhí)行前,會先將上面的SQL發(fā)送給數(shù)據(jù)庫進行編譯;執(zhí)行時,直接使用編譯好的SQL,替換占位符“?”就可以了。因為SQL注入只能對編譯過程起作用,所以這樣的方式就很好地避免了SQL注入的問題。
【底層實現(xiàn)原理】MyBatis是如何做到SQL預(yù)編譯的呢?其實在框架底層,是JDBC中的PreparedStatement類在起作用,PreparedStatement是我們很熟悉的Statement的子類,它的對象包含了編譯好的SQL語句。這種“準(zhǔn)備好”的方式不僅能提高安全性,而且在多次執(zhí)行同一個SQL時,能夠提高效率。原因是SQL已編譯好,再次執(zhí)行時無需再編譯