PDO防注入原理分析以及使用PDO的注意事項

咱們都知道,只要合理正確使用PDO,能夠基本上防止SQL注入的產生,本文主要回答如下兩個問題:

爲何要使用PDO而不是mysql_connect?

爲什麼PDO能防注入?

使用PDO防注入的時候應該特別注意什麼?

 

1、爲什麼要優先使用PDO?

PHP手冊上說得很清楚:

Prepared statements and stored procedures
Many of the more mature databases support the concept of prepared statements. What are they? They can be thought of as a kind of compiled template for the SQL that an application wants to run, that can be customized using variable parameters. Prepared statements offer two major benefits:

The query only needs to be parsed (or prepared) once, but can be executed multiple times with the same or different parameters. When the query is prepared, the database will analyze, compile and optimize its plan for executing the query. For complex queries this process can take up enough time that it will noticeably slow down an application if there is a need to repeat the same query many times with different parameters. By using a prepared statement the application avoids repeating the analyze/compile/optimize cycle. This means that prepared statements use fewer resources and thus run faster.

 


The parameters to prepared statements don't need to be quoted; the driver automatically handles this. If an application exclusively uses prepared statements, the developer can be sure that no SQL injection will occur (however, if other portions of the query are being built up with unescaped input, SQL injection is still possible).

 

即便用PDO的prepare方式,主要是提升相同SQL模板查詢性能、阻止SQL注入

同時,PHP手冊中給出了警告信息
Prior to PHP 5.3.6, this element was silently ignored. The same behaviour can be partly replicated with the PDO::MYSQL_ATTR_INIT_COMMAND driver option, as the following example shows.
Warning

The method in the below example can only be used with character sets that share the same lower 7 bit representation as ASCII, such as ISO-8859-1 and UTF-8. Users using character sets that have different representations (such as UTF-16 or Big5) must use the charset option provided in PHP 5.3.6 and later versions.

 

意思是說,在PHP 5.3.6及之前版本中,並不支持在DSN中的charset定義,而應該使用PDO::MYSQL_ATTR_INIT_COMMAND設置初始SQL, 即咱們經常使用的 set names gbk指令。

 

我看到一些程序,還在嘗試使用addslashes達到防注入的目的,卻不知這樣其實問題更多, 詳情請看http://www.lorui.com/addslashes-mysql_escape_string-mysql_real_eascape_string.html

還有一些作法:在執行數據庫查詢前,將SQL中的select, union, ....之類的關鍵詞清理掉。這種作法顯然是很是錯誤的處理方式,若是提交的正文中確實包含 the students's union , 替換後將篡改原本的內容,濫殺無辜,不可取。

 

2、爲什麼PDO能防SQL注入?php


請先看如下PHP代碼:

html

<?php

$pdo = new PDO("mysql:host=192.168.0.1;dbname=test;charset=utf8","root");

$st = $pdo->prepare("select * from info where id =? and name = ?");

$id = 21;

$name = 'zhangsan';

$st->bindParam(1,$id);

$st->bindParam(2,$name);

$st->execute();

$st->fetchAll();

?>


環境以下:

PHP 5.4.7

Mysql 協議版本 10

MySQL Server 5.5.27

爲了完全搞清楚php與mysql server通信的細節,我特別使用了wireshark抓包進行研究之,安裝wireshak以後,咱們設置過濾條件爲tcp.port==3306, 以下圖:mysql


 

如此只顯示與mysql 3306端口的通訊數據,避免沒必要要的干擾。

特別要注意的是wireshak基於wincap驅動,不支持本地環回接口的偵聽(即便用php鏈接本地mysql的方法是沒法偵聽的),請鏈接其它機器(橋接網絡的虛擬機也可)的MySQL進行測試。

而後運行咱們的PHP程序,偵聽結果以下,咱們發現,PHP只是簡單地將SQL直接發送給MySQL Server :
sql

 

其實,這與咱們平時使用mysql_real_escape_string將字符串進行轉義,再拼接成SQL語句沒有差異(只是由PDO本地驅動完成轉義的),顯然這種狀況下仍是有可能形成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,此項參數默認值未知。並且根據咱們剛剛抓包分析結果來看,php 5.3.6+默認仍是使用本地變量轉,拼接成SQL發送給MySQL Server的,咱們將這項值設置爲false, 試試效果,如如下代碼:

數據庫

<?php

$pdo = new PDO("mysql:host=192.168.0.1;dbname=test;","root");

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$st = $pdo->prepare("select * from info where id =? and name = ?");

$id = 21;

$name = 'zhangsan';

$st->bindParam(1,$id);

$st->bindParam(2,$name);

$st->execute();

$st->fetchAll();

?>

 


紅色行是咱們剛加入的內容,運行如下程序,使用wireshark抓包分析,得出的結果以下:安全

 


 


 

 

看到了嗎?這就是神奇之處,可見此次PHP是將SQL模板和變量是分兩次發送給MySQL的,由MySQL完成變量的轉義處理,既然變量和SQL模板是分兩次發送的,那麼就不存在SQL注入的問題了,但須要在DSN中指定charset屬性,如:

$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root');

如此,便可從根本上杜絕SQL注入的問題。若是你對此不是很清楚,能夠發郵件至zhangxugg@163.com, 一塊兒探討。


3、使用PDO的注意事項

知道以上幾點以後,咱們就能夠總結使用PDO杜絕SQL注入的幾個注意事項:

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屬性

3. 若是使用了PHP 5.3.6及之前版本,設置PDO::ATTR_EMULATE_PREPARES參數爲false(即由MySQL進行變量處理),php 5.3.6以上版本已經處理了這個問題,不管是使用本地模擬prepare仍是調用mysql server的prepare都可。在DSN中指定charset是無效的,同時set names <charset>的執行是必不可少的。


4. 若是使用了PHP 5.3.6及之前版本, 因Yii框架默認並未設置ATTR_EMULATE_PREPARES的值,請在數據庫配置文件中指定emulatePrepare的值爲false。


那麼,有個問題,若是在DSN中指定了charset, 是否還須要執行set names <charset>呢?

是的,不能省。set names <charset>其實有兩個做用:

A.  告訴mysql server, 客戶端(PHP程序)提交給它的編碼是什麼

B.  告訴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>指令。

若是圖片丟失,能夠發郵件至zhangxugg@163.com, 索取PDF版本。

我真想不通,一些新的項目,爲什麼不使用PDO而使用傳統的mysql_XXX函數庫呢?若是正確使用PDO,能夠從根本上杜絕SQL注入,我強烈建議各個公司的技術負責人、一線技術研發人員,要對這個問題引發重視,儘量使用PDO加快項目進度和安全質量。

不要再嘗試本身編寫SQL注入過濾函數庫了(又繁瑣並且很容易產生未知的漏洞)。
網絡

相關文章
相關標籤/搜索