一. SQL Injection及其防范的基本知識
可能大家都知道,SQL注入主要是利用字符型參數輸入的檢查漏洞。
比如說,程序中有這樣的查詢:
string sql = "SELECT * FROM SiteUsers WHERE UserName=" + userName + "";
其中的userName參數是從用戶界面上輸入的。
如果是正常的輸入,比如"Peter",SQL語句會串接成:
"SELECT * FROM SiteUsers WHERE UserName=Peter";
如果攻擊者輸入的是下面的字符串:
"xxx; DROP TABLE SiteUsers WHERE 1=1 or UserName=xxx"
此時SQL語句會變成下面這個樣子:
"SELECT * FROM SiteUsers WHERE UserName=xxx; DROP TABLE SiteUsers WHERE 1=1 or UserName=xxx";
其結果,得到執行的是兩個SQL語句,第二個語句的后果就比較嚴重了。
防止注入的方法其實很簡單,只要把用戶輸入的單引號變成雙份就行了:
string sql = "SELECT * FROM SiteUsers WHERE UserName=" + userName.Replace("","") + "";
這樣,如果輸入的是上面那種惡意參數,整個SQL語句會變成:
"SELECT * FROM SiteUsers WHERE UserName=xxx; DROP TABLE SiteUsers WHERE 1=1 or UserName=xxx";
被執行的還是一個SQL語句,整個粗體部分都成為參數值。
一般的做法,是在程序中統一調用下面這樣的共通函數,對參數進行處理:
private string SafeSqlLiteral(string inputSQL)
{
return inputSQL.Replace("", "");
}
由於很多人會疏忽這種單引號替換,所以真正安全的做法是使用參數化查詢。
二. 參數化查詢
在ADO.NET中,提供了一種參數化查詢方法,可以替代上面這種拼接SQL語句的做法。
參數化查詢的具體實現是:
(1)組織一個夾帶參數名的SQL語句,作為SqlCommand的CommandText。
(2)使用Parameters.Add方法設置參數值。
(3)執行SqlCommand。(這個步驟跟上面那種拼接SQL的辦法是一樣的。)
下面是一個例子:
string sql = "SELECT T2.dep_code, T2.dep_name FROM DEP ";
sql += " WHERE T2.dep_name like (%+ @Param + %) ";
SqlCommand sqlCommand = new SqlCommand(sql,cn);
sqlCommand.Parameters.Add(new SqlParameter("Param", s));
其中的@Param就是參數名,s則是用戶輸入的查詢條件字串。
(順便注:Oracle查詢語句參數用問號表示,不是"@參數名"的形式。)
使用這種參數化查詢的辦法,防止SQL注入的任務就交給ADO.Net了。
如果在項目中統一規定必須使用參數化查詢,就不用擔心因個別程序員的疏忽導致的SQL注入漏洞了。
但是,問題還沒有完,SQL注入的漏洞是堵住了,但是查詢結果的正確性,參數化查詢並不能幫上什么忙。
三. 通配符問題
如果使用LIKE語句進行模糊查詢,會有一些特殊的通配符問題。
SQL Server的通配符包括下划線(_)和百分號(%),分別表示單個字符和任意多字符。
如果用戶輸入參數中包括這些通配符,就會出現結果不正確的問題。
比如說:
WHERE T2.name like (%+ @Param + %)
如果用戶輸入下划線,他期待的結果應該是name字段值含有下划線的記錄,但是結果是所有記錄都會被查詢出來。輸入百分號也是如此。
為此,在將用戶輸入的內容作為參數值傳入之前,必須進行通配符的轉義處理(英文叫做Escape),也就是說,如果用戶輸入的查詢條件中含有通配符,必須將這些字符作為數據而不是通配符來對待。
在SQL Server的查詢語句中,將通配符轉義為普通數據的方法是用方括號括起來。
比如說,如果想要查詢帶有下划線的字段,正確的寫法是:
WHERE T2.name like (%+ [_] + %)
同樣,如果想要查詢帶有百分號的字段,正確的寫法是:
WHERE T2.name like (%+ [%] + %)
所以,即使使用參數化查詢,也必須在將用戶輸入的內容當作參數值傳入SqlCommand.Parameters之前,先進行下面的處理:
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");
四. 方括號問題
如果你足夠細心,可能發現了還有一個方括號問題。
既然方括號是用來界定數據內容的,那么如果用戶輸入的查詢參數本身就包括方括號時,會出現什么結果呢?
根據用戶的期望,如果輸入一個方括號,查詢結果中應該只包括那些字段值中含有方括號的記錄。
但是實驗結果表明,如果是沒有配成對的單個左方括號,查詢時這個左方括號會被忽略。
也就是說,下面這個語句:
WHERE T2.name like (%+ [ + %)
等價於下面這個語句:
WHERE T2.name like (%+ + %)
這將導致查詢結果中包含表中的全部記錄,就像沒有任何過濾條件一樣。
為此,如果用戶輸入的查詢條件中含有左方括號的話,還必須對左方括號進行轉義:
s = s.Replace("[", "[[]");
注:右方括號沒有這個問題。
五. 其他注意事項
按照微軟的建議,凡是有可能導致問題的輸入,可以在UI部分就進行檢查並拒掉。
這些可疑輸入包括:
分號(;):多個查詢語句之間的分隔符,注入攻擊時的惡意查詢語句往往就是第二個查詢語句。
單引號():字符串數據分隔符,這是最危險的,前面已經討論了。
注釋符(–或者/*,*/):有些數據庫可以利用注釋設置一些查詢引擎的行為,比如如何利用索引等。
xp_:擴展存儲過程的前綴,SQL注入攻擊得手之后,攻擊者往往會通過執行xp_cmdshell之類的擴展存儲過程,獲取系統信息,甚至控制、破壞系統。
六、結論
為了防止SQL注入,同時避免用戶輸入特殊字符時查詢結果不准確的問題,應該做兩件事:
(1)使用參數化查詢。
(2)在使用用戶輸入的字符串數據設置查詢參數值之前,首先調用下面的共通處理函數:
private static string ConvertSql(string sql)
{
//sql = sql.Replace("'", "''"); // ADO.NET已經做了,不要自己做
sql = sql.Replace("[", "[[]"); // 這句話一定要在下面兩個語句之前,否則作為轉義符的方括號會被當作數據被再次處理
sql = sql.Replace("_", "[_]");
sql = sql.Replace("%", "[%]");
return sql;
}