使用PDO防sql注入的原理分析
前言
本文使用pdo的預處理方式可以避免sql注入。下面話不多說了,來一起看看詳細的介紹吧
在php手冊中'PDO--預處理語句與儲存過程'下的說明:
- 很多更成熟的資料庫都支援預處理語句的概念。什麼是預處理語句?可以把它看作是想要執行的 SQL 的一種編譯過的模板,它可以使用變數引數進行定製。預處理語句可以帶來兩大好處:
- 查詢僅需解析(或預處理)一次,但可以用相同或不同的引數執行多次。當查詢準備好後,資料庫將分析、編譯和優化執行該查詢的計劃。對於複雜的查詢,此過程要花費較長的時間,如果需要以不同引數多次重複相同的查詢,那麼該過程將大大降低應用程式的速度。通過使用預處理語句,可以避免重複分析/編譯/優化周 期。簡言之,預處理語句佔用更少的資源,因而執行得更快。
- 提供給預處理語句的引數不需要用引號括起來,驅動程式會自動處理。如果應用程式只使用預處理語句,可以確保不會發生SQL 注入。(然而,如果查詢的其他部分是由未轉義的輸入來構建的,則仍存在 SQL 注入的風險)。
- 預處理語句如此有用,以至於它們唯一的特性是在驅動程式不支援的時PDO 將模擬處理。這樣可以確保不管資料庫是否具有這樣的功能,都可以確保應用程式可以用相同的資料訪問模式。
下邊分別說明一下上述兩點好處:
1.首先說說mysql的儲存過程,mysql5中引入了儲存過程特性,儲存過程建立的時候,資料庫已經對其進行了一次解析和優化。其次,儲存過程一旦執行,在記憶體中就會保留一份這個儲存過程,這樣下次再執行同樣的儲存過程時,可以從記憶體中直接中讀取。mysql儲存過程的使用可以參看:https://www.jb51.net/article/7032.htm
對於PDO,原理和其相同,只是PDO支援EMULATE_PREPARES(模擬預處理)方式,是在本地由PDO驅動完成,同時也可以不使用本地的模擬預處理,交由mysql完成,下邊會對這兩種情況進行說明。
2.防止sql注入,我通過tcpdump和wireshark結合抓包來分析一下。
在虛擬機器上執行一段程式碼,對遠端mysql發起請求:
<?php $pdo = new PDO("mysql:host=10.121.95.81;dbname=thor_cms;charset=utf8","root","qihoo@360@qihoo"); $st = $pdo->prepare("select * from share where id =? and uid = ?"); $id = 6; $uid = 521; $st->bindParam(1,$id); $st->bindParam(2,$uid); $st->execute(); $ret = $st->fetchAll(); print_r($ret);
通過tcpdump抓包生成檔案:
tcpdump -ieth0 -A -s 3000 port 3306 -w ./mysql.dump sz mysql.dump
通過wireshark開啟檔案:
可以看到整個過程:3次握手--Login Request--Request Query--Request Quit
檢視Request Query包可以看到:
咦?這不也是拼接sql語句麼?
其實,這與我們平時使用mysql_real_escape_string將字串進行轉義,再拼接成SQL語句沒有差別,只是由PDO本地驅動完成轉義的(EMULATE_PREPARES)
這種情況下還是有可能造成SQL 注入的,也就是說在php本地呼叫pdo prepare中的mysql_real_escape_string來操作query,使用的是本地單位元組字符集,而我們傳遞多位元組編碼的變數時,有可能還是會造成SQL注入漏洞(php 5.3.6以前版本的問題之一,這也就解釋了為何在使用PDO時,建議升級到php 5.3.6+,並在DSN字串中指定charset的原因)。
針對php 5.3.6以前版本,以下程式碼仍然可能造成SQL注入問題:
$pdo->query('SET NAMES GBK'); $var = chr(0xbf) . chr(0x27) . " OR 1=1 /*"; $query = "SELECT * FROM info WHERE name = ?"; $stmt = $pdo->prepare($query); $stmt->execute(array($var));
而正確的轉義應該是給mysql Server指定字符集,並將變數傳送給MySQL Server完成根據字元轉義。
那麼,如何才能禁止PHP本地轉義而交由MySQL Server轉義呢?
PDO有一項引數,名為PDO::ATTR_EMULATE_PREPARES ,表示是否使用PHP本地模擬prepare,此項引數預設true,我們改為false後再抓包看看。
先在程式碼第一行後新增
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
再次用tcpdump抓包,通過wireshark我們可以看到:
php對sql語句傳送採用了prepare--execute方式
這次的變數轉義處理交由mysql server來執行。
既然變數和SQL模板是分兩次傳送的,那麼就不存在SQL注入的問題了,但明顯會多一次傳輸,這在php5.3.6之後是不需要的。
使用PDO的注意事項
1. php升級到5.3.6+,生產環境強烈建議升級到php 5.3.9+ php 5.4+,php 5.3.8存在致命的hash碰撞漏洞。
2. 若使用php 5.3.6+,請在在PDO的DSN中指定charset屬性。小於5.3.6 : $dbh = new PDO($dsn,$user,$pass,array(PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8"));
3. 如果使用了PHP 5.3.6及以前版本,設定PDO::ATTR_EMULATE_PREPARES引數為false(即由MySQL server進行變數處理),php 5.3.6以上版本已經處理了這個問題,無論是使用本地模擬prepare還是呼叫mysql server的prepare均可。
4. 如果使用了PHP 5.3.6及以前版本,因Yii框架預設並未設定ATTR_EMULATE_PREPARES的值,請在資料庫配置檔案中指定emulatePrepare的值為false。
注:
1.為什麼在DSN中指定了charset,還需要執行set names <charset>呢?
其實set names <charset>有兩個作用:
告訴mysql server,客戶端(PHP程式)提交給它的編碼是什麼
告訴mysql server,客戶端需要的結果的編碼是什麼
也就是說,如果資料表使用gbk字符集,而PHP程式使用UTF-8編碼,我們在執行查詢前執行set names utf8,告訴mysql server正確編碼即可,無須在程式中編碼轉換。這樣我們以utf-8編碼提交查詢到mysql server,得到的結果也會是utf-8編碼。省卻了程式中的轉換編碼問題,不要有疑問,這樣做不會產生亂碼。
那麼在DSN中指定charset的作用是什麼? 只是告訴PDO,本地驅動轉義時使用指定的字符集(並不是設定mysql server通訊字符集),設定mysql server通訊字符集,還得使用set names <charset>指令。
2.PDO::ATTR_EMULATE_PREPARES屬性設定為false引發的血案:http://my.oschina.net/u/437615/blog/369481
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對我們的支援。