在PHP中全面阻止SQL注入式攻擊之三
一、 建立一個(gè)安全抽象層
我們并不建議你手工地把前面介紹的技術(shù)應(yīng)用于每一個(gè)用戶輸入的實(shí)例中,而是強(qiáng)烈推薦你為此創(chuàng)建一個(gè)抽象層。一個(gè)簡(jiǎn)單的抽象是把你的校驗(yàn)方案加入到一個(gè)函數(shù)中,并且針對(duì)用戶輸入的每一項(xiàng)調(diào)用這個(gè)函數(shù)。當(dāng)然,我們還可以創(chuàng)建一種更復(fù)雜的更高一級(jí)的抽象-把一個(gè)安全的查詢封裝到一個(gè)類中,從而應(yīng)用于整個(gè)應(yīng)用程序。在網(wǎng)上已經(jīng)存在許多這種現(xiàn)成的免費(fèi)的類;在本篇中,我們正要討論其中的一些。
進(jìn)行這種抽象至少存在三個(gè)優(yōu)點(diǎn)(而且每一個(gè)都會(huì)改進(jìn)安全級(jí)別):
1. 本地化代碼。
2. 使查詢的構(gòu)造更快且更為可靠-因?yàn)檫@可以把部分工作交由抽象代碼來(lái)實(shí)現(xiàn)。
3. 當(dāng)基于安全特征進(jìn)行構(gòu)建并且恰當(dāng)使用時(shí),這將會(huì)有效地防止我們前面所討論的各種各樣的注入式攻擊。
二、 改進(jìn)現(xiàn)有的應(yīng)用程序
如果你想改進(jìn)一個(gè)現(xiàn)有的應(yīng)用程序,則使用一個(gè)簡(jiǎn)單的抽象層是最適當(dāng)?shù)摹R粋€(gè)能夠簡(jiǎn)單地'清理'你所收集的任何用戶輸入內(nèi)容的函數(shù)可能看起來(lái)如下所示:
function safe( $string ) { return ''' . mysql_real_escape_string( $string ) . '''}
【注意】我們已經(jīng)構(gòu)建了相應(yīng)于值要求的單引號(hào)以及mysql_real_escape_string()函數(shù)。接下來(lái),就可以使用這個(gè)函數(shù)來(lái)構(gòu)造一個(gè)$query變量,如下所示:
$variety = safe( $_POST['variety'] );$query = ' SELECT * FROM wines WHERE variety=' . $variety;
現(xiàn)在,你的用戶試圖進(jìn)行一個(gè)注入式攻擊-通過(guò)輸入下列內(nèi)容作為變量$variety的值:
lagrein' or 1=1;
注意,如果不進(jìn)行上面的'清理',則最后的查詢將如下所示(這將導(dǎo)致無(wú)法預(yù)料的結(jié)果):
SELECT * FROM wines WHERE variety = 'lagrein' or 1=1;'
然而現(xiàn)在,既然用戶的輸入已經(jīng)被清理,那么查詢語(yǔ)句就成為下面這樣一種無(wú)危害的形式:
SELECT * FROM wines WHERE variety = 'lagrein' or 1=1;'
既然數(shù)據(jù)庫(kù)中不存在與指定的值相應(yīng)的variety域(這正是惡意用戶所輸入的內(nèi)容-lagrein' or 1=1;),那么,這個(gè)查詢將不能返回任何結(jié)果,并且注入將會(huì)失敗。
三、 保護(hù)一個(gè)新的應(yīng)用程序
如果你正在創(chuàng)建一個(gè)新的應(yīng)用程序,那么,你可以從頭開始創(chuàng)建一個(gè)安全抽象層。如今,PHP 5新改進(jìn)的對(duì)于MySQL的支持(這主要體現(xiàn)在新的mysqli擴(kuò)展中)為這種安全特征提供了強(qiáng)有力的支持(既有過(guò)程性的,也有面向?qū)ο筇卣鞯模?。你可以從站點(diǎn)http://php.net/mysqli上獲取有關(guān)mysqli的信息。注意,只有當(dāng)你使用--with-mysqli=path/to/mysql_config選項(xiàng)編譯PHP時(shí),這種mysqli支持才可用。下面是該代碼的一個(gè)過(guò)程性版本,用于保護(hù)一個(gè)基于mysqli的查詢:
<?php //檢索用戶的輸入 $animalName = $_POST['animalName']; //連接到數(shù)據(jù)庫(kù) $connect = mysqli_connect( 'localhost', 'username', 'password', 'database' ); if ( !$connect ) exit( 'connection failed: ' . mysqli_connect_error() ); //創(chuàng)建一個(gè)查詢語(yǔ)句源 $stmt = mysqli_prepare( $connect,'SELECT intelligence FROM animals WHERE name = ?' ); if ( $stmt ) {//把替代綁定到語(yǔ)句上mysqli_stmt_bind_param( $stmt, 's', $animalName );//執(zhí)行該語(yǔ)句mysqli_stmt_execute( $stmt );//檢索結(jié)果...mysqli_stmt_bind_result( $stmt, $intelligence );// ...并顯示它if ( mysqli_stmt_fetch( $stmt ) ) { print 'A $animalName has $intelligence intelligence.n';} else { print 'Sorry, no records found.';}//清除語(yǔ)句源mysqli_stmt_close( $stmt ); } mysqli_close( $connect );?>
該mysqli擴(kuò)展提供了一組函數(shù)用于構(gòu)造和執(zhí)行查詢。而且,它也非常準(zhǔn)確地提供了前面使用我們自己的safe()函數(shù)所實(shí)現(xiàn)的功能。
在上面的片斷中,首先收集用戶提交的輸入內(nèi)容并建立數(shù)據(jù)庫(kù)連接。然后,使用mysqli_prepare()函數(shù)創(chuàng)建一個(gè)查詢語(yǔ)句源-在此命名為$stmt以反映使用它的函數(shù)的名稱。這個(gè)函數(shù)使用了兩個(gè)參數(shù):連接資源和一個(gè)字符串(每當(dāng)你使用擴(kuò)展插入一個(gè)值時(shí),'?'標(biāo)記被插入到其中)。在本例中,你僅有一個(gè)這樣的值-動(dòng)物的名字。
注意,在一個(gè)SELECT語(yǔ)句中,放置'?'標(biāo)記的唯一的有效位置是在值比較部分。這正是為什么你不需要指定使用哪個(gè)變量的原因(除了在mysqli_stmt_bind_param()函數(shù)中之外)。在此,你還需要指定它的類型-在本例中,'s'代表字符串。其它可能的類型有:'I'代表整數(shù),'d'代表雙精度數(shù)(或浮點(diǎn)數(shù)),而'b'代表二進(jìn)制字符串。
函數(shù)mysqli_stmt_execute(),mysqli_stmt_bind_result()和mysqli_stmt_fetch()負(fù)責(zé)執(zhí)行查詢并檢索結(jié)果。如果存在檢索結(jié)果,則顯示它們;如果不存在結(jié)果,則顯示一條無(wú)害的消息。最后,你需要關(guān)閉$stmt資源以及數(shù)據(jù)庫(kù)連接-從內(nèi)存中對(duì)它們加以釋放。
假定一個(gè)合法的用戶輸入了字符串'lemming',那么這個(gè)例程將(假定是數(shù)據(jù)庫(kù)中適當(dāng)?shù)臄?shù)據(jù))輸出消息'A lemming has very low intelligence.'。假定存在一個(gè)嘗試性注入-例如'lemming' or 1=1;',那么這個(gè)例程將打印(無(wú)害)消息'Sorry, no records found.'。此外,mysqli擴(kuò)展還提供了一個(gè)面向?qū)ο蟀姹镜南嗤睦獭O旅妫覀兿胝f(shuō)明這種版本的使用方法。
<?php $animalName = $_POST['animalName']; $mysqli = new mysqli( 'localhost', 'username', 'password', 'database'); if ( !$mysqli ) exit( 'connection failed: ' . mysqli_connect_error() ); $stmt = $mysqli->prepare( 'SELECT intelligence FROM animals WHERE name = ?' ); if ( $stmt ) {$stmt->bind_param( 's', $animalName );$stmt->execute();$stmt->bind_result( $intelligence );if ( $stmt->fetch() ) { print 'A $animalName has $intelligence intelligence.n';} else { print 'Sorry, no records found.';}$stmt->close(); } $mysqli->close();?>
實(shí)際上,這部分代碼是前面描述代碼的復(fù)制-它使用了一種面向?qū)ο蟮恼Z(yǔ)法和組織方法,而不是嚴(yán)格的過(guò)程式代碼。四、 更高級(jí)的抽象
如果你使用外部庫(kù)PearDB,那么,你可以對(duì)應(yīng)用程序的安全保護(hù)模塊進(jìn)行全面的抽象。
另一方面,使用這個(gè)庫(kù)存在一個(gè)突出的缺點(diǎn):你只能受限于某些人的思想,而且代碼管理方面也添加了大量的工作。為此,在決定是否使用它們之前,你需要進(jìn)行仔細(xì)地斟酌。如果你決定這樣做,那么,你至少確保它們能夠真正幫助你'清理'你的用戶輸入的內(nèi)容。
五、 測(cè)試你的注入式保護(hù)能力
正如我們?cè)谇懊嫠懻摰?,確保你的腳本安全的一個(gè)重要的部分是對(duì)它們進(jìn)行測(cè)試。為此,最好的辦法是你自己創(chuàng)建SQL代碼注入測(cè)試。在此,我們提供了一個(gè)這種測(cè)試的示例。在本例中,我們測(cè)試對(duì)一個(gè)SELECT語(yǔ)句的注入式攻擊。
<?php//被測(cè)試的保護(hù)函數(shù)function safe( $string ) { return ''' . mysql_real_escape_string( $string ) . '''}//連接到數(shù)據(jù)庫(kù)/////////////////////////試圖進(jìn)行注入///////////////////////$exploit = 'lemming' AND 1=1;';//進(jìn)行清理$safe = safe( $exploit );$query = 'SELECT * FROM animals WHERE name = $safe';$result = mysql_query( $query );//測(cè)試是否保護(hù)是足夠的if ( $result && mysql_num_rows( $result ) == 1 ) { exitt 'Protection succeeded:n exploit $exploit was neutralized.';}else { exit( 'Protection failed:n exploit $exploit was able to retrieve all rows.' );}?>
如果你想創(chuàng)建這樣的一個(gè)測(cè)試集,并試驗(yàn)基于不同的SQL命令的各種不同的注入,那么,你將會(huì)很快地探測(cè)出你的保護(hù)策略中的任何漏洞。一旦糾正這些問(wèn)題,那么,你就可以很有把握-你已經(jīng)建立起真正的注入式攻擊保護(hù)機(jī)制。
六、 小結(jié)
在本系列文章一開始,我們通過(guò)一個(gè)SQL注入討論分析了對(duì)你的腳本的特定威脅-由不恰當(dāng)?shù)挠脩糨斎胨隆V?,我們描述了SQL注入的工作原理并精確地分析了PHP是怎樣易于被注入的。然后,我們提供了一個(gè)實(shí)際中的注入示例。之后,我們推薦一系列措施來(lái)使試圖的注入攻擊變?yōu)闊o(wú)害的-這將分別通過(guò)確保使所有提交的值以引號(hào)封閉,通過(guò)檢查用戶提交值的類型,以及通過(guò)過(guò)濾掉你的用戶輸入的潛在危險(xiǎn)的字符等方法來(lái)實(shí)現(xiàn)的。最后,我們推薦,你最好對(duì)你的校驗(yàn)例程進(jìn)行抽象,并針對(duì)更改一個(gè)現(xiàn)有應(yīng)用程序提供了腳本示例。然后,我們討論了第三方抽象方案的優(yōu)缺點(diǎn)。
全文完
