網(wǎng)絡(luò)安全現(xiàn)在越來越受到重視,其中后端應(yīng)用最常見的安全漏洞就是SQL注入,據(jù)owasp(開放式Web應(yīng)用程序安全項(xiàng)目組織)的安全漏洞統(tǒng)計(jì),注入漏洞常年排名TOP 3。為了盡可能地減少SQL注入問題,我們進(jìn)行了一些實(shí)踐總結(jié),并在部門內(nèi)部進(jìn)行了推廣。
SQL注入原理
SQL注入攻擊是通過操作輸入來修改SQL語句,以達(dá)到執(zhí)行注入的SQL、進(jìn)而對(duì)web服務(wù)器進(jìn)行攻擊的方法。簡(jiǎn)單的說就是在web表單或頁(yè)面請(qǐng)求的查詢字符串中插入SQL命令, 最終使web服務(wù)器執(zhí)行惡意命令的過程。可以通過一個(gè)例子簡(jiǎn)單說明SQL注入攻擊。假設(shè)某網(wǎng)站頁(yè)面顯示時(shí)URL為http://www.example.com?test=123,此時(shí)URL實(shí)際向服務(wù)器傳遞了值為123的變量test,這表明當(dāng)前頁(yè)面是對(duì)數(shù)據(jù)庫(kù)進(jìn)行動(dòng)態(tài)查詢的結(jié)果。如果服務(wù)器使用了SQL動(dòng)態(tài)拼接,此時(shí)可以在URL中插入惡意SQL語句并執(zhí)行。例如預(yù)期執(zhí)行語句是:
select * from user where testId = 123
如果有人惡意構(gòu)造test=123 or 1 = 1;那么最后實(shí)際執(zhí)行的語句就是:
select * from user where testId = 123 or 1 = 1
SQL注入本質(zhì)是利用動(dòng)態(tài)SQL拼接并直接執(zhí)行,并且前端輸入的參數(shù)未經(jīng)過嚴(yán)格校驗(yàn)直接參與SQL拼接,輸入?yún)?shù)被惡意利用的話就存在不受控的命令注入風(fēng)險(xiǎn)。SQL注入會(huì)攻擊數(shù)據(jù)庫(kù),所以危害是非常大的,SQL注入的危害包括:1. 數(shù)據(jù)庫(kù)信息泄露;2. 數(shù)據(jù)庫(kù)惡意操作攻擊服務(wù)器;3. 刪除和修改數(shù)據(jù)庫(kù)表信息;4. 服務(wù)器遠(yuǎn)程控制等。防SQL注入原則根據(jù)SQL注入的原理,我們總結(jié)了如下防SQL注入的原則。1)盡可能參數(shù)化執(zhí)行SQL,使用PreparedStatement;2)無法參數(shù)化執(zhí)行就對(duì)輸入?yún)?shù)使用強(qiáng)類型接收,如枚舉;3)如果既不能參數(shù)化也無法用強(qiáng)類型,就做好格式校驗(yàn);4)如果上面都做不到,最后考慮做好注入字符的轉(zhuǎn)義過濾。
防SQL注入實(shí)踐總結(jié)
- 實(shí)踐1:使用數(shù)據(jù)庫(kù)預(yù)編譯SQL執(zhí)行
關(guān)系型數(shù)據(jù)庫(kù)都提供了預(yù)編譯執(zhí)行SQL語句的方式,和直接執(zhí)行有參數(shù)拼接的SQL方式不同的是,在SQL預(yù)編譯階段,所有參數(shù)都會(huì)被模板化,參數(shù)值不直接參與編譯,這樣參數(shù)值就無法改變SQL的結(jié)構(gòu),包括語句條數(shù),參數(shù)個(gè)數(shù),查詢條件等等,就算參數(shù)里存在惡意注入?yún)?shù)值也無法破壞原來的SQL結(jié)構(gòu)。下面舉例說明拼接執(zhí)行和參數(shù)化執(zhí)行的區(qū)別。直接執(zhí)行:
select name from user_info where name = 'gouge'
使用預(yù)編譯執(zhí)行(也就是PreparedStatement)分兩步。
第一步:模板化編譯
select name from user_info where name = ?
第二步:參數(shù)帶入
select name from user_info where name = 'gouge'
預(yù)編譯后的語句執(zhí)行時(shí),就算參數(shù)值里有惡意注入,也只是當(dāng)成一個(gè)字符串的一部分,并不會(huì)導(dǎo)致SQL結(jié)構(gòu)被破壞,所以這種方式能夠從根本上杜絕SQL注入的風(fēng)險(xiǎn),也是目前公認(rèn)防SQL注入最好的辦法。
- 實(shí)踐2: 可選情況下,優(yōu)先使用非String類型作為入?yún)?/span>
SQL注入依賴于SQL動(dòng)態(tài)拼接,如果非String(包括枚舉)參數(shù)有強(qiáng)類型嚴(yán)格校驗(yàn),拼接的時(shí)候就沒有SQL注入的風(fēng)險(xiǎn)
- 實(shí)踐3:對(duì)String類型參數(shù)進(jìn)行枚舉校驗(yàn)
對(duì)于可枚舉的String入?yún)?,在后端需要進(jìn)行枚舉值白名單校驗(yàn),防止偽造。以經(jīng)常使用的排序字段為例,排序字段不少情況下是從前端傳過來的,不可信。所以需要做枚舉校驗(yàn),看看傳過來的排序字段是否在預(yù)期內(nèi)。
- 實(shí)踐4:存儲(chǔ)過程
存儲(chǔ)過程因?yàn)橐彩穷A(yù)編譯的,所以也可以防止SQL注入,不過存儲(chǔ)過程已經(jīng)是不推薦的實(shí)踐了,這里只是列舉一下。
- 實(shí)踐5:對(duì)String入?yún)⑦M(jìn)行轉(zhuǎn)義處理
這個(gè)實(shí)踐方案的優(yōu)先級(jí)最低,前面的方案都無法實(shí)施或代價(jià)太大的情況下,才考慮使用,因?yàn)檫@個(gè)辦法并不是完全可靠的。這個(gè)辦法是把用戶輸入拼到SQL之前,先對(duì)輸入進(jìn)行轉(zhuǎn)義,所以實(shí)現(xiàn)上是和數(shù)據(jù)庫(kù)綁定的,不同的數(shù)據(jù)庫(kù)轉(zhuǎn)義實(shí)現(xiàn)都不一樣。原理是對(duì)特定的查詢,數(shù)據(jù)庫(kù)支持一種或多種轉(zhuǎn)義的方法,如果對(duì)所有用戶輸入的參數(shù)拼到SQL之前進(jìn)行合適的轉(zhuǎn)義,就可以防止SQL注入,但是不能保證所有情況都能處理完美。OWASP組織提供了可以直接使用的java類庫(kù)esapi,可以用來完成轉(zhuǎn)義,目前支持mysql,oracle,還不支持sqlserver,OWASP官方文檔中也有說明這個(gè)類庫(kù)也不能保證100%可以防SQL注入。
Mybatis防SQL注入
Mybatis的XML Mapper中參數(shù)有兩種表達(dá)方式: #{xxx}和${xxx},區(qū)別是#{xxx}會(huì)被模板化為參數(shù)化形式(?),所以參數(shù)值不參與編譯;${xxx}會(huì)被替換為對(duì)應(yīng)的參數(shù)值參與編譯。所以使用#{xxx}語法的參數(shù)沒有注入風(fēng)險(xiǎn),但是使用${xxx}不當(dāng),就存在注入風(fēng)險(xiǎn)。我們使用sonar規(guī)則來檢測(cè)${xxx}的使用情況,發(fā)現(xiàn)很多可以使用#{xxx}的情況下,卻使用了${xxx},大概是使用者還不了解其中的區(qū)別和風(fēng)險(xiǎn)。根據(jù)防止SQL注入的原則,針對(duì)使用Mybatis作為ORM的時(shí)候,我們可以得出這樣的結(jié)論:
- 在xml中能使用參數(shù)化#{}語法實(shí)現(xiàn)的,禁止使用${}
- 不得已必須使用${}語法拼接SQL時(shí),必須先進(jìn)行合法校驗(yàn)和轉(zhuǎn)義
Mybatis-XML常見誤用及安全建議
1)常規(guī)誤用風(fēng)險(xiǎn)寫法:
account = '${account}'
安全寫法:
account = #{account}
2) in 參數(shù)拼接風(fēng)險(xiǎn)寫法:
WHERE id IN (${item.ids})
安全寫法:
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach
collection="ids"
item="id"
open="("
separator=","
close=")">
#{id}
</foreach>
</if>
3) sqlserver top語法風(fēng)險(xiǎn)寫法:
top ${queryCnt}
安全寫法:
top (#{queryCnt})
4)between語法風(fēng)險(xiǎn)寫法:
BETWEEN ${start} AND ${end}
安全寫法:
BETWEEN #{start} AND #{end}
5)like語法風(fēng)險(xiǎn)寫法:
email like '%${emailSuffix}'
安全寫法1:
email like concat('%', #{emailSuffix})
安全寫法2:
<bind name="pattern" value="'%'+ emailSuffix">
Email like #{pattern}
安全寫法3:在java代碼中構(gòu)造好pattern,使用#{}語法6) limit語法風(fēng)險(xiǎn)寫法:
limit ${offset}, ${size}
安全寫法:
limit #{offset}, #{size}
7) 動(dòng)態(tài)order by風(fēng)險(xiǎn)寫法:
order by ${orderByClause}
安全建議1:可以在xml中使用if代替java中拼湊orderby(推薦);安全建議2:對(duì)排序字段和排序類型值進(jìn)行白名單校驗(yàn);8)動(dòng)態(tài)表名安全建議:建議實(shí)現(xiàn)Mybatis插件,在框架層面對(duì)表名進(jìn)行替換處理,避免在xml使用${}來拼湊表名。9) Mybatis example語法風(fēng)險(xiǎn)用法:
<if test="orderByClause != null">
order by ${orderByClause}
</if>
Mybatis生成的example使用,大多數(shù)情況下是安全的,但是orderByCluase是通過字符串拼接的,也是不安全的,所以不建議使用。安全建議:可以考慮使用mybatis新出的mybatis-dynamic-sql或者國(guó)內(nèi)開源的mybatis-plus,比example語法更簡(jiǎn)潔,也更安全。例如:
Query.orderBy(createAt.descending());
WAF除了在SQL語句層面盡量保證防止SQL注入風(fēng)險(xiǎn)之外,基礎(chǔ)設(shè)施WAF(Web Application Firewall)也是非常有效的防止SQL注入的措施,有條件的也可以考慮部署WAF。
總結(jié)
本文主要是記錄了經(jīng)銷商技術(shù)部在安全方面防止SQL注入的一些實(shí)踐探索和總結(jié),包括SQL注入的原理,防SQL注入總體遵循的原則,以及具體的實(shí)踐中的安全建議,希望對(duì)其他人也有所幫助。