PDO防sql注入原理分析

使用pdo的預處理方式能夠避免sql注入。php

在php手冊中'PDO--預處理語句與存儲過程'下的說明:mysql

  • 不少更成熟的數據庫都支持預處理語句的概念。什麼是預處理語句?能夠把它看做是想要運行的 SQL 的一種編譯過的模板,它可使用變量參數進行定製。預處理語句能夠帶來兩大好處:
  • 查詢僅需解析(或預處理)一次,但能夠用相同或不一樣的參數執行屢次。當查詢準備好後,數據庫將分析、編譯和優化執行該查詢的計劃。對於複雜的查詢,此過程要花費較長的時間,若是須要以不一樣參數屢次重複相同的查詢,那麼該過程將大大下降應用程序的速度。經過使用預處理語句,能夠避免重複分析/編譯/優化周 期。簡言之,預處理語句佔用更少的資源,於是運行得更快。
  • 提供給預處理語句的參數不須要用引號括起來,驅動程序會自動處理。若是應用程序只使用預處理語句,能夠確保不會發生SQL 注入。(然而,若是查詢的其餘部分是由未轉義的輸入來構建的,則仍存在 SQL 注入的風險)。
  • 預處理語句如此有用,以致於它們惟一的特性是在驅動程序不支持的時PDO 將模擬處理。這樣能夠確保無論數據庫是否具備這樣的功能,均可以確保應用程序能夠用相同的數據訪問模式。  

 

下邊分別說明一下上述兩點好處:sql

1.首先說說mysql的存儲過程,mysql5中引入了存儲過程特性,存儲過程建立的時候,數據庫已經對其進行了一次解析和優化。其次,存儲過程一旦執行,在內存中就會保留一份這個存儲過程,這樣下次再執行一樣的存儲過程時,能夠從內存中直接中讀取。mysql存儲過程的使用能夠參看:http://maoyifa100.iteye.com/blog/1900305數據庫

對於PDO,原理和其相同,只是PDO支持EMULATE_PREPARES(模擬預處理)方式,是在本地由PDO驅動完成,同時也能夠不使用本地的模擬預處理,交由mysql完成,下邊會對這兩種狀況進行說明。
框架

2.防止sql注入,我經過tcpdump和wireshark結合抓包來分析一下。tcp

在虛擬機上執行一段代碼,對遠端mysql發起請求:fetch

<?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打開文件:ui

能夠看到整個過程: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

 

參考:http://zhangxugg-163-com.iteye.com/blog/1835721

相關文章
相關標籤/搜索