PDO準備好的語句是否足以防止SQL注入?

假設我有這樣的代碼: php

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

PDO文檔說: html

準備好的語句的參數不須要用引號引發來。 司機爲您處理。 mysql

那真的是我避免SQL注入所須要作的一切嗎? 真的那麼容易嗎? git

您能夠假設MySQL會有所做爲。 另外,我真的只是對針對SQL注入使用準備好的語句感到好奇。 在這種狀況下,我不在意XSS或其餘可能的漏洞。 github


#1樓

不,這還不夠(在某些特定狀況下)! 默認狀況下,當使用MySQL做爲數據庫驅動程序時,PDO使用模擬的準備好的語句。 使用MySQL和PDO時,應始終禁用模擬的準備好的語句: sql

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

老是應該作的另外一件事是它設置數據庫的正確編碼: 數據庫

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

另請參閱如下相關問題: 如何防止PHP中的SQL注入? c#

還要注意,這僅與數據庫方面有關,在顯示數據時您仍然須要注意本身。 例如,以正確的編碼和引用樣式再次使用htmlspecialchars()安全


#2樓

簡短的回答是「 否」 ,PDO準備將不會爲您防護全部可能的SQL注入攻擊。 對於某些晦澀的邊緣狀況。 服務器

我正在修改此答案以談論PDO ...

長答案不是那麼容易。 它基於此處演示的攻擊。

攻擊

所以,讓咱們開始展現攻擊...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

在某些狀況下,它將返回1行以上。 讓咱們剖析這裏發生的事情:

  1. 選擇字符集

    $pdo->query('SET NAMES gbk');

    爲了使這種攻擊起做用,咱們須要服務器在鏈接上指望的編碼既編碼爲'如ASCII即0x27 也要具備某些字符的最終字節爲ASCII \\0x5c 。 事實證實,默認狀況下,MySQL 5.6默認支持5種此類編碼: big5cp932gb2312gbksjis 。 咱們將在此處選擇gbk

    如今,在這裏注意SET NAMES的使用很是重要。 這將在服務器上設置字符集。 還有另外一種方法,可是咱們會盡快到達那裏。

  2. 有效載荷

    咱們將用於此注入的有效負載從字節序列0xbf27 。 在gbk ,這是一個無效的多字節字符; 在latin1 ,它是字符串¿' 。 請注意,在latin1 gbk0x27自身是一個文本'字符。

    咱們選擇此有效負載是由於,若是咱們在其上調用addslashes() ,則會在'字符以前插入一個ASCII \\0x5c 。 所以,咱們將得到0xbf5c27 ,它在gbk是兩個字符序列: 0xbf5c後跟0x27 。 換句話說,就是一個有效字符,後跟一個未轉義的' 。 可是咱們沒有使用addslashes() 。 繼續下一步...

  3. $ stmt-> execute()

    這裏要意識到的重要一點是,默認狀況下,PDO 不會執行真正的預處理語句。 它模擬它們(對於MySQL)。 所以,PDO在內部構建查詢字符串,對每一個綁定的字符串值調用mysql_real_escape_string() (MySQL C API函數)。

    mysql_real_escape_string()的C API調用與addslashes() mysql_real_escape_string()不一樣之處在於,它知道鏈接字符集。 所以,它能夠爲服務器指望的字符集正確執行轉義。 可是,到目前爲止,客戶端認爲咱們仍在使用latin1進行鏈接,由於咱們從未告訴過它。 咱們確實告訴服務器咱們正在使用gbk ,可是客戶端仍然認爲它是latin1

    所以,對mysql_real_escape_string()的調用將插入反斜槓,而且在「轉義」內容中有一個自由懸掛的'字符! 實際上,若是咱們要在gbk字符集中查看$var ,則會看到:

    OR'OR 1 = 1 / *

    這正是攻擊所須要的。

  4. 查詢

    這只是一個形式,但這是呈現的查詢:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

恭喜,您剛剛使用PDO Prepared Statements成功攻擊了一個程序...

簡單修復

如今,值得注意的是,能夠經過禁用模擬的準備好的語句來防止這種狀況:

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

一般會致使產生真正準備好的語句(即,將數據發送到與查詢分開的數據包中)。 可是,要知道,PDO會悄悄地退回到仿真陳述,MySQL不能原生準備:那些可被在手冊中,但要注意選擇合適的服務器版本)。

正確的解決方法

這裏的問題是咱們沒有調用C API的mysql_set_charset()而不是SET NAMES 。 若是這樣作的話,若是咱們從2006年開始使用MySQL版本,咱們會很好的。

若是您使用的是較早的MySQL版本,則mysql_real_escape_string()錯誤意味着出於轉義目的,無效的多字節字符(例如,咱們的有效負載中的字符)被視爲單個字節, 即便已正確告知客戶端鏈接編碼等,也是如此。此次攻擊仍然會成功。 該錯誤是固定在MySQL 4.1.205.0.225.1.11

可是最糟糕的是, PDO直到5.3.6才公開mysql_set_charset()的C API,所以在之前的版本中,它沒法針對全部可能的命令阻止這種攻擊! 如今它做爲DSN參數公開,應該代替 SET NAMES

拯救的恩典

正如咱們在一開始所說的那樣,要使這種攻擊起做用,必須使用易受攻擊的字符集對數據庫鏈接進行編碼。 utf8mb4 並不是易受攻擊 ,但能夠支持每一個 Unicode字符:所以您能夠選擇使用它,可是它僅自MySQL 5.5.3起可用。 utf8是一種替代方法,它也不易受攻擊 ,能夠支持整個Unicode Basic Multilingual Plane

另外,您能夠啓用NO_BACKSLASH_ESCAPES SQL模式,該模式(除其餘外)會更改mysql_real_escape_string()的操做。 啓用此模式後,會將0x27替換爲0x2727而不是0x5c27 ,所以轉義過程沒法使用之前不存在的任何易受攻擊的編碼建立有效字符(即0xbf27仍爲0xbf27等),所以服務器仍將拒絕該字符串爲無效。 可是,請參閱@eggyal的答案 ,以瞭解使用此SQL模式可能會引發的其餘漏洞(儘管不是PDO)。

安全的例子

如下示例是安全的:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

由於服務器指望utf8 ...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

由於咱們已經正確設置了字符集,因此客戶端和服務器匹配。

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

由於咱們已經關閉了模擬的準備好的語句。

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

由於咱們已經正確設置了字符集。

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

由於MySQLi始終會執行真正的預備語句。

包起來

若是你:

  • 使用MySQL的現代版本(5.1版,全部5.5版,5.6版等) PDO的DSN字符集參數(在PHP≥5.3.6中)

要麼

  • 不要使用易受攻擊的字符集進行鏈接編碼(您只能使用utf8 / latin1 / ascii等)

要麼

  • 啓用NO_BACKSLASH_ESCAPES SQL模式

您是100%安全的。

不然, 即便您使用的是PDO預準備語句 ,也容易受到攻擊

附錄

我一直在緩慢地開發一個補丁程序,以更改默認值,以不模仿未來的PHP版本。 我遇到的問題是,當我這樣作時,不少測試都失敗了。 一個問題是,模擬的Prepare只會在執行時拋出語法錯誤,而真正的Prepare則會在Prepare上拋出錯誤。 所以,這可能會致使問題(這是測試很乏味的部分緣由)。


#3樓

就我的而言,我老是會首先對數據進行某種形式的衛生處理,由於您永遠不會信任用戶輸入,可是當使用佔位符/參數綁定時,輸入的數據將分別發送到服務器的sql語句,而後綁定在一塊兒。 此處的關鍵是,這會將提供的數據綁定到特定類型和特定用途,並消除了更改SQL語句邏輯的任何機會。


#4樓

準備好的語句/參數化查詢一般足以防止對該語句*進行 一階注入。 若是在應用程序的其餘任何地方使用未經檢查的動態sql,則仍然容易受到二階注入的攻擊。

2階注入意味着數據在包含在查詢中以前已經在數據庫中循環了一次,而且很難提取。 據我所知,你幾乎歷來沒有看到真正的工程二階攻擊,由於它是攻擊者的社會工程師他們的方式一般更容易,但你有時有2次錯誤裁剪,由於額外的良性達'的字符或類似。

當您可使一個值存儲在數據庫中,該數據庫之後用做查詢中的文字時,就能夠完成二階注入攻擊。 舉例來講,假設您在網站上建立賬戶時輸入如下信息做爲新的用戶名(假設使用MySQL DB解決此問題):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

若是對用戶名沒有其餘限制,則一條準備好的語句仍將確保上述嵌入式查詢在插入時不會執行,並將值正確存儲在數據庫中。 可是,請想象一下,稍後應用程序將從數據庫中檢索您的用戶名,並使用字符串串聯將該值包括在新查詢中。 您可能會看到別人的密碼。 因爲用戶表中的前幾個名稱一般是管理員,所以您可能也剛剛放棄了服務器場。 (還請注意:這是不將密碼存儲爲純文本的另外一個緣由!)

咱們看到,那麼,準備語句是足以讓一個單一的查詢,但它們自己並不足以防止SQL注入攻擊貫穿整個應用程序,由於他們缺少一種機制,以執行該應用程序使用中的全部對數據庫的訪問安全代碼。 可是,用做良好應用程序設計的一部分(其中可能包括諸如代碼審查或靜態分析之類的實踐,或者使用限制動態sql的ORM,數據層或服務層), 準備好的語句 解決Sql Injection的主要工具問題。 若是您遵循良好的應用程序設計原則,從而將數據訪問與程序的其他部分分開,則能夠輕鬆實施或審覈每一個查詢正確使用參數化的過程。 在這種狀況下,徹底防止了sql注入(一階和二階)。


*事實證實,當涉及到寬字符時,MySql / PHP只是(愚蠢)只是在處理參數方面很愚蠢,在這裏另外一個極受好評的答案中仍然概述了一種罕見的狀況,這種狀況能夠容許注入經過參數化來進行查詢。


#5樓

是的,足夠了。 注入式攻擊的工做方式是經過某種方式使解釋器(數據庫)評估應該是數據的某些東西,就好像它是代碼同樣。 僅當您在同一媒介中混合使用代碼和數據時(例如,將查詢構造爲字符串時),纔有可能。

參數化查詢經過分別發送代碼和數據來工做,所以永遠不可能在其中發現漏洞。

可是,您仍然可能容易受到其餘注入式攻擊的攻擊。 例如,若是您使用HTML頁面中的數據,則可能會受到XSS類型的攻擊。

相關文章
相關標籤/搜索