在PHP 5.3.6及以前版本中,並不支援在DSN中的charset定義,而應該使用PDO::MYSQL_ATTR_INIT_COMMAND設定初始SQL, 即我們常用的 set names gbk指令。
為何PDO能防SQL注入?
請先看以下PHP代碼:
代碼如下 |
複製代碼 |
<?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只是簡單地將SQL直接發送給MySQL Server,其實,這與我們平時使用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; //www.111Cn.nEt $name = 'zhangsan'; $st->bindParam(1,$id); $st->bindParam(2,$name); $st->execute(); $st->fetchAll(); ?> |
這次PHP是將SQL模板和變數是分兩次發送給MySQL的,由MySQL完成變數的轉義處理,既然變數和SQL模板是分兩次發送的,那麼就不存在SQL注入的問題了,但需要在DSN中指定charset屬性,如:
代碼如下 |
複製代碼 |
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root'); |
如此,即可從根本上杜絕SQL注入的問題。
使用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進行變數處理),在DSN中指定charset是無效的,同時set names <charset>(此處詳細語句PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8')的執行是必不可少的。(php 5.3.6以上版本已經處理了這個問題,無論是使用本地類比prepare還是調用mysql server的prepare均可。)
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>指令。
以下是一段可防注入的範例程式碼:
代碼如下 |
複製代碼 |
$dbhost="localhost"; $dbname="test"; $dbusr="root"; $dbpwd=""; $dbhdl=NULL; $dbstm=NULL; $opt = array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',); $dsn='mysql:host=' . $dbhost . ';dbname=' . $dbname.';charset=utf8'; try { $dbhdl = new PDO($dsn, $dbusr, $dbpwd, $opt);//www.111cn.net $dbhdl=->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); //dbhdl->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT);//Display none //dbhdl->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_WARNING);//Display warning $dbhdl->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);//Display exception } catch (PDOExceptsddttrtion $e) {//return PDOException print "Error!: " . $e->getMessage() . "<br>"; die(); } $dbhost="localhost"; $dbname="test"; $dbusr="root"; $dbpwd=""; $dbhdl=NULL; $dbstm=NULL; $dsn='mysql:host=' . $dbhost . ';dbname=' . $dbname.';charset=utf8'; try { $dbhdl = new PDO($dsn, $dbusr, $dbpwd,); $dbhdl=->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); //dbhdl->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT);//Display none //dbhdl->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_WARNING);//Display warning $dbhdl->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);//Display exception $dbhdl->query('SET NAMES GBK'); } catch (PDOExceptsddttrtion $e) {//return PDOException print "Error!: " . $e->getMessage() . "<br>"; die(); }
|