PHP安全編程:防止SQL注入

SQL 注入是PHP應用中最多見的漏洞之一。事實上使人驚奇的是,開發者要同時犯兩個錯誤纔會引起一個SQL注入漏洞,一個是沒有對輸入的數據進行過濾(過濾輸入),還有一個是沒有對發送到數據庫的數據進行轉義(轉義輸出)。這兩個重要的步驟缺一不可,須要同時加以特別關注以減小程序錯誤。php

對於攻擊者來講,進行SQL注入攻擊須要思考和試驗,對數據庫方案進行有根有據的推理很是有必要(固然假設攻擊者看不到你的源程序和數據庫方案),考慮如下簡單的登陸表單:mysql

1 <form action="/login.php" method="POST">
2 <p>Username: <input type="text" name="username" /></p>
3 <p>Password: <input type="password" name="password" /></p>
4 <p><input type="submit" value="Log In" /></p>
5 </form>

做爲一個攻擊者,他會從推測驗證用戶名和密碼的查詢語句開始。經過查看源文件,他就能開始猜想你的習慣。算法

好比命名習慣。一般會假設你表單中的字段名爲與數據表中的字段名相同。固然,確保它們不一樣未必是一個可靠的安全措施。sql

第一次猜想,通常會使用下面例子中的查詢:數據庫

01 <?php
02   
03 $password_hash = md5($_POST['password']);
04   
05 $sql = "SELECT count(*)
06       FROM   users
07       WHERE  username = '{$_POST['username']}'
08       AND    password = '$password_hash'";
09   
10 ?>

使用用戶密碼的MD5值原來是一個通行的作法,但如今並非特別安全了。最近的研究代表MD5算法有缺陷,並且大量MD5數據庫下降了MD5反向破解的難度。請訪問http://md5.rednoize.com/ 查看演示(原文如此,山東大學教授王小云的研究代表能夠很快的找到MD5的「碰撞」,就是能夠產生相同的MD5值的不一樣兩個文件和字串。MD5是信息摘要算法,而不是加密算法,反向破解也就無從談起了。不過根據這個成果,在上面的特例中,直接使用md5是危險的。)。安全

最好的保護方法是在密碼上附加一個你本身定義的字符串,例如:服務器

1 <?php
2   
3 $salt 'SHIFLETT';
4 $password_hash = md5($salt . md5($_POST['password'] . $salt));
5   
6 ?>

固然,攻擊者未必在第一次就能猜中,他們經常還須要作一些試驗。有一個比較好的試驗方式是把單引號做爲用戶名錄入,緣由是這樣可能會暴露一些重要信息。有不少開發人員在Mysql語句執行出錯時會調用函數mysql_error()來報告錯誤。見下面的例子:數據庫設計

1 <?php
2   
3 mysql_query($sqlor exit(mysql_error());
4   
5 ?>

雖然該方法在開發中十分有用,但它能向攻擊者暴露重要信息。若是攻擊者把單引號作爲用戶名,mypass作爲密碼,查詢語句就會變成:函數

1 <?php
2   
3 $sql = "SELECT *
4       FROM   users
5       WHERE  username = '''
6       AND    password = 'a029d0df84eb5549c641e04a9ef389e5'";
7   
8 ?>

當該語句發送到MySQL後,系統就會顯示以下錯誤信息:加密

1 You have an error in your SQL syntax. Check the manual that corresponds to your
2 MySQL server version for the right syntax to use near 'WHERE username = ''' AND
3 password = 'a029d0df84eb55

不費吹灰之力,攻擊者已經知道了兩個字段名(username和password)以及他們出如今查詢中的順序。除此之外,攻擊者還知道了數據沒有正確進行過濾(程序沒有提示非法用戶名)和轉義(出現了數據庫錯誤),同時整個WHERE條件的格式也暴露了,這樣,攻擊者就能夠嘗試操縱符合查詢的記錄了。

在這一點上,攻擊者有不少選擇。一是嘗試填入一個特殊的用戶名,以使查詢不管用戶名密碼是否符合,都能獲得匹配:

1 myuser' or 'foo' = 'foo' --

假定將mypass做爲密碼,整個查詢就會變成:

1 <?php
2   
3 $sql = "SELECT *
4       FROM   users
5       WHERE  username = 'myuser' or 'foo' 'foo' --
6       AND    password = 'a029d0df84eb5549c641e04a9ef389e5'";
7   
8 ?>

因爲中間插入了一個SQL註釋標記,因此查詢語句會在此中斷。這就容許了一個攻擊者在不知道任何合法用戶名和密碼的狀況下登陸。

若是知道合法的用戶名,攻擊者就能夠該用戶(如chris)身份登陸。只要chris是合法的用戶名,攻擊者就能夠控制該賬號。緣由是查詢變成了下面的樣子:

1 <?php
2 $sql = "SELECT *
3       FROM   users
4       WHERE  username = 'chris' --
5       AND    password = 'a029d0df84eb5549c641e04a9ef389e5'";
6 ?>

幸運的是,SQL注入是很容易避免的。正如前面所說起的,你必須堅持過濾輸入和轉義輸出。

雖然兩個步驟都不能省略,但只要實現其中的一個就能消除大多數的SQL注入風險。若是你只是過濾輸入而沒有轉義輸出,你極可能會遇到數據庫錯誤(合法的數據也可能影響SQL查詢的正確格式),但這也不可靠,合法的數據還可能改變SQL語句的行爲。另外一方面,若是你轉義了輸出,而沒有過濾輸入,就能保證數據不會影響SQL語句的格式,同時也防止了多種常見SQL注入攻擊的方法。

固然,仍是要堅持同時使用這兩個步驟。過濾輸入的方式徹底取決於輸入數據的類型(見第一章的示例),但轉義用於向數據庫發送的輸出數據只要使用同一個函數便可。對於MySQL用戶,可使用函數mysql_real_escape_string( ):

01 <?php
02   
03 $clean array();
04 $mysql array();
05   
06 $clean['last_name'] = "O'Reilly";
07 $mysql['last_name'] = mysql_real_escape_string($clean['last_name']);
08   
09 $sql = "INSERT
10       INTO   user (last_name)
11       VALUES ('{$mysql['last_name']}')";
12   
13 ?>

儘可能使用爲你的數據庫設計的轉義函數。若是沒有,使用函數addslashes()是最終的比較好的方法。

當全部用於創建一個SQL語句的數據被正確過濾和轉義時,實際上也就避免了SQL注入的風險。若是你正在使用支持參數化查詢語句和佔位符的數據庫操做類(如PEAR::DB, PDO等),你就會多獲得一層保護。見下面的使用PEAR::DB的例子:

1 <?php
2 $sql = 'INSERT
3       INTO   user (last_name)
4       VALUES (?)';
5 $dbh->query($sqlarray($clean['last_name']));
6 ?>

因爲在上例中數據不能直接影響查詢語句的格式,SQL注入的風險就下降了。PEAR::DB會自動根據你的數據庫的要求進行轉義,因此你只須要過濾輸出便可。

若是你正在使用參數化查詢語句,輸入的內容就只會做爲數據來處理。這樣就沒有必要進行轉義了,儘管你可能認爲這是必要的一步(若是你但願堅持轉義輸出習慣的話)。實際上,這時是否轉義基本上不會產生影響,由於這時沒有特殊字符須要轉換。在防止SQL注入這一點上,參數化查詢語句爲你的程序提供了強大的保護。

注:關於SQL注入,不得不說的是如今大多虛擬主機都會把magic_quotes_gpc選項打開,在這種狀況下全部的客戶端GET和POST的數據都會自動進行addslashes處理,因此此時對字符串值的SQL注入是不可行的,但要防止對數字值的SQL注入,如用intval()等函數進行處理。但若是你編寫的是通用軟件,則須要讀取服務器的magic_quotes_gpc後進行相應處理。

相關文章
相關標籤/搜索