一個是沒有對輸入的資料進行過濾(過濾輸入),還有一個是沒有對發送到資料庫的資料進行轉義(轉義輸出)。這兩個重要的步驟缺一不可,需要同時加以特別關注以減少程式錯誤。
對於攻擊者來說,進行SQL注入攻擊需要思考和實驗,對資料庫方案進行有根有據的推理非常有必要(當然假設攻擊者看不到你的來源程式和資料庫方案),考慮以下簡單的登入表單:
複製代碼 代碼如下:
<form action="/login.php" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="Log In" /></p>
</form>
作為一個攻擊者,他會從推測驗證使用者名稱和密碼的查詢語句開始。通過查看源檔案,他就能開始猜測你的習慣。
比如命名習慣。通常會假設你表單中的欄位名為與資料表中的欄位名相同。當然,確保它們不同未必是一個可靠的安全措施。
第一次猜測,一般會使用下面例子中的查詢:
複製代碼 代碼如下:
<?php
$password_hash = md5($_POST['password']);
$sql = "SELECT count(*)
FROM users
WHERE username = '{$_POST['username']}'
AND password = '$password_hash'";
?>
使用使用者密碼的MD5值原來是一個通行的做法,但現在並不是特別安全了。最近的研究表明MD5演算法有缺陷,而且大量MD5資料庫降低了MD5反向破解的難度。請訪問http://md5.rednoize.com/ 查看示範(原文如此,山東大學教授王小雲的研究表明可以很快的找到MD5的“碰撞”,就是可以產生相同的MD5值的不同兩個檔案和字串。MD5是資訊摘要演算法,而不是密碼編譯演算法,反向破解也就無從談起了。不過根據這個成果,在上面的特例中,直接使用md5是危險的。)。
最好的保護方法是在密碼上附加一個你自己定義的字串,例如:
複製代碼 代碼如下:
<?php
$salt = 'SHIFLETT';
$password_hash = md5($salt . md5($_POST['password'] . $salt));
?>
當然,攻擊者未必在第一次就能猜中,他們常常還需要做一些實驗。有一個比較好的實驗方式是把單引號作為使用者名稱錄入,原因是這樣可能會暴露一些重要訊息。有很多開發人員在Mysql語句執行出錯時會調用函數mysql_error()來報告錯誤。見下面的例子:
複製代碼 代碼如下:
<?php
mysql_query($sql) or exit(mysql_error());
?>
雖然該方法在開發中十分有用,但它能向攻擊者暴露重要訊息。如果攻擊者把單引號做為使用者名稱,mypass做為密碼,查詢語句就會變成:
複製代碼 代碼如下:
<?php
$sql = "SELECT *
FROM users
WHERE username = '''
AND password = 'a029d0df84eb5549c641e04a9ef389e5'";
?>
當該語句發送到MySQL後,系統就會顯示如下錯誤資訊:
複製代碼 代碼如下:
You have an error in your SQL syntax. Check the manual that corresponds to your
MySQL server version for the right syntax to use near 'WHERE username = ''' AND
password = 'a029d0df84eb55
不費吹灰之力,攻擊者已經知道了兩個欄位名(username和password)以及他們出現在查詢中的順序。除此以外,攻擊者還知道了資料沒有正確進行過濾(程式沒有提示非法使用者名稱)和轉義(出現了資料庫錯誤),同時整個WHERE條件的格式也暴露了,這樣,攻擊者就可以嘗試操縱符合查詢的記錄了。
在這一點上,攻擊者有很多選擇。一是嘗試填入一個特殊的使用者名稱,以使查詢無論使用者名稱密碼是否符合,都能得到匹配:
複製代碼 代碼如下:
myuser' or 'foo' = 'foo' --
假定將mypass作為密碼,整個查詢就會變成:
複製代碼 代碼如下:
<?php
$sql = "SELECT *
FROM users
WHERE username = 'myuser' or 'foo' = 'foo' --
AND password = 'a029d0df84eb5549c641e04a9ef389e5'";
?>
幸運的是,SQL注入是很容易避免的。正如前面所提及的,你必須堅持過濾輸入和轉義輸出。
雖然兩個步驟都不能省略,但只要實現其中的一個就能消除大多數的SQL注入風險。如果你只是過濾輸入而沒有轉義輸出,你很可能會遇到資料庫錯誤(合法的資料也可能影響SQL查詢的正確格式),但這也不可靠,合法的資料還可能改變SQL語句的行為。另一方面,如果你轉義了輸出,而沒有過濾輸入,就能保證資料不會影響SQL語句的格式,同時也防止了多種常見SQL注入攻擊的方法。
當然,還是要堅持同時使用這兩個步驟。過濾輸入的方式完全取決於輸入資料的類型(見第一章的樣本),但轉義用於向資料庫發送的輸出資料只要使用同一個函數即可。對於MySQL使用者,可以使用函數mysql_real_escape_string( ):
複製代碼 代碼如下:
<?php
$clean = array();
$mysql = array();
$clean['last_name'] = "O'Reilly";
$mysql['last_name'] = mysql_real_escape_string($clean['last_name']);
$sql = "INSERT
INTO user (last_name)
VALUES ('{$mysql['last_name']}')";
?>
盡量使用為你的資料庫設計的轉義函數。如果沒有,使用函數addslashes()是最終的比較好的方法。
當所有用於建立一個SQL語句的資料被正確過濾和轉義時,實際上也就避免了SQL注入的風險。如果你正在使用支援參數化查詢語句和預留位置的資料庫操作類(如PEAR::DB, PDO等),你就會多得到一層保護。見下面的使用PEAR::DB的例子:
複製代碼 代碼如下:
<?php
$sql = 'INSERT
INTO user (last_name)
VALUES (?)';
$dbh->query($sql, array($clean['last_name']));
?>
由於在上例中資料不能直接影響查詢語句的格式,SQL注入的風險就降低了。PEAR::DB會自動根據你的資料庫的要求進行轉義,所以你只需要過濾輸出即可。
如果你正在使用參數化查詢語句,輸入的內容就只會作為資料來處理。這樣就沒有必要進行轉義了,儘管你可能認為這是必要的一步(如果你希望堅持轉義輸出習慣的話)。實際上,這時是否轉義基本上不會產生影響,因為這時沒有特殊字元需要轉換。在防止SQL注入這一點上,參數化查詢語句為你的程式提供了強大的保護。
註:關於SQL注入,不得不說的是現在大多虛擬機器主機都會把magic_quotes_gpc選項開啟,在這種情況下所有的用戶端GET和POST的資料都會自動進行addslashes處理,所以此時對字串值的SQL注入是不可行的,但要防止對數字值的SQL注入,如用intval()等函數進行處理。但如果你編寫的是通用軟體,則需要讀取伺服器的magic_quotes_gpc後進行相應處理。