在日常開發過程中,開發人員一般只關心 SQL 是否能實現預期的功能,而對於 SQL 的安全問題一般都不太重視。實際上,如果 SQL 語句寫作不當,將會給應用系統造成很大的安全隱患,其中最重要的隱患就是 SQL 注入。接下來我們將會對 SQL 注入以及相應的防範措施進行詳細的介紹。
SQL 注入簡介
結構化查詢語言(SQL)是一種用來和數據庫交互的文本語言。SQL Injection 就是利用某些數據庫的外部接口將用戶數據插入到實際的數據庫操作語言(SQL)當中,從而達到入侵數據庫乃至操作系統的目的。
它的產生主要是由於程序對用戶輸入的數據沒有進行嚴格的過濾,導致非法數據庫查詢語句的執行。
SQL 注入(SQL Injection)攻擊具有很大的危害,攻擊者可以利用它讀取、修改或者刪除數據庫內的數據,獲取數據庫中的用戶名和密碼等敏感信息,甚至可以獲得數據庫管理員的權限,而且,SQL Injection 也很難防範。網站管理員無法通過安裝系統補丁或者進行簡單的安全配置進行自我保護,一般的防火牆也無法攔截 SQL Injection 攻擊。
下面的用戶登錄驗證程序就是 SQL 注入的一個例子(以 PHP 程序舉例)。
首先創建用戶表 user:
CREATE TABLE user (
userid int(11) NOT NULL auto_increment,
username varchar(20) NOT NULL default '',
password varchar(20) NOT NULL default '',
PRIMARY KEY (userid)
) ENGINE=MyISAM AUTO_INCREMENT=3 ;
給用戶表 user 添加一條用戶記錄:
INSERT INTO `user` VALUES (1, 'angel', 'mypass');
驗證用戶 root 登錄 localhost 服務器:
<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "test";
mysql_connect($servername,$dbusername,$dbpassword) or die ("數據庫連接失敗");
$sql = "SELECT * FROM user WHERE username='$username' AND password='$password'";
$result = mysql_db_query($dbname, $sql);
$userinfo = mysql_fetch_array($result);
if (empty($userinfo)){
echo "登錄失敗";
} else {
echo "登錄成功";
}
echo "<p>SQL Query:$sql<p>";
?>
(4)然後提交如下 URL:
http://127.0.0.1/injection/user.php?username=angel' or '1=1
結果發現,這個 URL 可以成功登錄系統,但是很顯然這並不是我們預期的結果。同樣也可以利用 SQL 的註釋語句實現 SQL 注入,如下面的例子:
http://127.0.0.1/injection/user.php?username=angel'/*
http://127.0.0.1/injection/user.php?username=angel'#
因爲在 SQL 語句中,“/*”或者“#”都可以將後面的語句註釋掉。這樣上述語句就可以通過這兩個註釋符中任意一個將後面的語句給註釋掉了,結果導致只根據用戶名而沒有密碼的 URL 都成功進行了登錄。
利用“or”和註釋符的不同之處在於,前者是利用邏輯運算,而後者則是根據 MySQL 的特性,這個比邏輯運算簡單得多了。雖然這兩種情況實現的原理不同,但是達到了同樣的 SQL 注入效果,都是我們應該關注的。
應用開發中可以採取的應對措施
PrepareStatement+Bind-variable
可以參考我之前的博文 :《JSP | 使用預處理語句對象操作數據庫》
對 Java、JSP 開發的應用,可以使用 PrepareStatement+Bind-variable 來防止 SQL 注入,另外從 PHP 5 開始,也在擴展的 MySQLI 中支持 PrepareStatement,所以在使用這類語言作數據庫開發時,強烈建議使用 PrepareStatement+Bind-variable 來實現,而儘量不要使用拼接的SQL,下面以 Java 爲例說明一下實現方法:
…
String sql = "select * from users u where u.id = ? and u.password = ?";
preparedstatement ps = connection.preparestatement(sql);
ps.setint(1,id);
ps.setstring(2,pwd);
resultset rs = ps.executequery();
…
使用應用程序提供的轉換函數
很多應用程序接口都提供了對特殊字符進行轉換的函數,恰當地使用這些函數,可以防止應用程序用戶輸入使應用程序生成不期望的語句。
- MySQL C API:使用 mysql_real_escape_string() API 調用。
- MySQL++:使用 escape 和 quote 修飾符。
- PHP:使用 mysql_real_escape_string()函數(適用於 PHP 4.3.0 版本)。從 PHP 5 開始,可以使用擴展的 MySQLI,這是對 MySQL 新特性的一個擴展支持,其中的一個優點就是支持PrepareStatement。
- Perl DBI:使用 placeholders 或者 quote()方法。
- Ruby DBI:使用 placeholders 或者 quote()方法。
自己定義函數進行校驗
如果現有的轉換函數仍然不能滿足要求,則需要自己編寫函數進行輸入校驗。輸入驗證是一個很複雜的問題。輸入驗證的途徑可以分爲以下幾種:
- 整理數據使之變得有效;
- 拒絕已知的非法輸入;
- 只接受已知的合法輸入。
所以如果想要獲得最好的安全狀態,目前最好的解決辦法就是對用戶提交或者可能改變的數據進行簡單分類,分別應用正則表達式來對用戶提供的輸入數據進行嚴格的檢測和驗證。
下面採用正則表達式的方法提供一個驗證函數,以供參考。
已知非法符號有:
“’”、“;”、“=”、“(”、“)”、“/*”、“*/”、
“%”、“+”、“”、“>”、“<”、“--”、“[”、“]”;
其實只需要過濾非法的符號組合就可以阻止已知形式的攻擊,並且如果發現更新的攻擊符號組合,也可以將這些符號組合增添進來,繼續防範新的攻擊。特別是空格符號和與其產生相同作用的分隔關鍵字的符號,例如“/**/”,如果能成功過濾這種符號,那麼有很多注入攻擊將不能發生,並且同時也要過濾它們的十六進制表示“%XX”。
由此可以構造如下正則表達式:
(|\'|(\%27)|\;|(\%3b)|\=|(\%3d)|\(|(\%28)|\)|(\%29)|(\/*)|(\%2f%2a)|(\*/)|(\%2a%2f)|\+| (\
%2b)|\<|(\%3c)|\>|(\%3e)|\(--))|\[|\%5b|\]|\%5d)
根據上述的正則表達式,可以提供一個函數(以 PHP 舉例),可以防範大多數的 SQL 注入,具體函數如下:
function SafeRequest ($ParaName, $ParaType)
{
/* ---傳入參數--- */
/* ParaName:參數名稱-字符型 */
/* ParaType: 參數類型-數字型(1 表示參數是數字或字符,0 表示參數爲其他)*/
if ($ParaType == 1)
{
$re = "/[^\w+$]/";
}
else
{
$re = "/(|\ '
|(\%27)|\;|(\%3b)|\=|(\%3d)|\(|(\%28)|\)|(\%29)|(\/*)|(\%2f%2a)|(*/)|(\%2a%2f)|\+|(\%2b)|\<|(\%3c)|\>|(\%3e)|\(--))|\[|\%5b|\]|\%5d)/";
}
if (preg_match($re, $ParaName) > 0)
{
echo("參數不符合要求,請重新輸入!");
return 0;
}
else
{
return 1;
}
}