SQL經常和程序代碼一塊兒使用。咱們一般所說的SQL動態查詢,是指將程序中的變量和基本SQL語句拼接成一個完整的查詢語句。html
string sql = SELECT * FROM Person WHERE Id = $Id
咱們指望$Id是一個整型,所以當數據庫接收到這個請求時,$Id的值就是查詢語句的一部分。git
SQL動態查詢是有效利用數據庫很天然的方法。當你使用程序內的變量來指定如何進行查詢時,就是將SQL做爲鏈接程序和數據庫的橋樑。程序和數據庫之間經過這種方式進行「對話」。程序員
然而,要讓程序按照你想要的方式執行並不難,難的是讓程序變得安全,不執行你不想讓它執行的操做。但軟件在收到SQL注入攻擊時,一般都沒法保證安全。web
當往SQL查詢的字符串中插入別的內容,而這些被插入的內容以你不但願的方式修改了查詢語句的語法時,SQL注入就成功了。如,對於上面所寫的SQL語句,$Id的值變爲"123; DELETE FROM Person"。那麼最終的查詢語句會變成這樣:sql
SELECT * ROM Person WHERE Id = 123; DELETE FORM Person
若是你的程序真的執行了這樣一行SQL語句,那麼悲劇了,Person表裏的數據就徹底被清空了。數據庫
一、對Web安全的嚴重威脅編程
當攻擊者可以使用SQL注入操控你的SQL查詢時,它就變成了一個巨大的威脅。假設你的數據庫中修改密碼的SQL語句是這樣寫的:後端
UPDATE Account SET Password = SHA2('$password') WHERE AccountId = 123;
那麼聰明的攻擊者會猜想你請求參數對應在SQL語句中的做用,而且精心選擇每一個參數對應的值:安全
http://www.xxx.com/setpassword?password=123456&userid=123 OR TRUE
若是你的程序真的被攻擊者所繞過了,那麼你的數據庫將會執行以下SQL語句:oracle
UPDATE Account SET Password = 123456 WHERE AccountId = 123 OR TRUE
Account表中,全部用戶的密碼都被改爲了123456。
有數不盡的方法選擇一個惡意的字符串來改變SQL語句的行爲。它只受制於攻擊者的想象力和你保護SQL語句的能力。
二、尋找治癒良方
有不少文章,聲稱某一種技術室對抗SQL注入的萬能藥。而事實上,這些技術都被證實沒法阻擋全部類型的SQL注入。所以你須要在不一樣的狀況下,將全部這些技術組合起來使用。
(1).轉義
防止SQL語句包含任何不匹配的引號是最古老的方法,就是對全部的引號字符進行轉義操做,使它們不至於成爲字符串的結束符。在標準SQL語句中,可使用兩個連續的單引號來表示一個單引號字符:
SELECT * FROM Person WHERE Name = 'O''Hare'
大多數數據庫還支持使用反斜槓對單引號進行轉義操做:
SELECT * FROM Person WHERE Name = 'O\'Hare'
這麼作的原理是,將應用程序中的數據插入到SQL語句以前就進行轉換,大多數SQL的編程接口都會提供一個簡便的函數來作這個操做。
這樣作了以後,全部的字符串都會被包上引號,若是對上面修改帳號的SQL語句這樣操做的話,SQL語句會變成這樣:
UPDATE Account SET Password = SHA2('123456') WHERE AccountId = '123 OR TRUE'
在SQL中,是沒有辦法讓一個數值列直接和一個帶有數字的字符串進行比較的,無論是哪一種數據庫,這都不能夠。在標準SQL中,將字符串轉換爲數字時,必須明確使用CASE()函數,所以上面的SQL語句只是報個錯誤,還不至於所有用戶帳號的密碼都被修改了。
(2).查詢參數
一個常常被認爲是防止SQL語句注入的萬能解決方案是使用"參數化查詢",不一樣於在SQL語句中插入動態內容,查詢參數的作法是在準備查詢語句的時候,在對應參數的地方使用參數佔位符。隨後,在執行這個預先準備好的查詢時提供一個參數。
cmd.CommandText = "Update Person Set Name = 'Ado.net' WHERE Id = @Id"; //設置操做語句 cmd.Parameters.Add("@Id", SqlDbType.Int); //添加參數 cmd.Parameters["@Id"].Value = 1; //設置參數值
大多數開發人員都推薦這個方案,由於你不須要對動態內容進行轉義,或者擔憂有缺陷的轉義函數。
事實上,查詢參數這個方法的確是對付SQL注入一個強勁有效的解決方案。但這並非一個通用的方案,由於查詢參數老是被視爲一個字面值。
多個值的列表不能夠當成單一參數:
cmd.CommandText = "Update Person Set Name = 'Ado.net' WHERE Id IN (@Id)"; //設置操做語句 cmd.Parameters.Add("@Id", SqlDbType.String); //添加參數 cmd.Parameters["@Id"].Value = "1,2,3"; //設置參數值
這個作法會致使數據庫認爲傳入的是一個包含數字和逗號的字符串,處理過程將和一系列整數做爲參數進行查詢並不同。
真正生成到數據庫中執行的SQL語句爲:
Update Person Set Name = 'Ado.net' WHERE Id IN ('1,2,3')
表名沒法做爲參數:
cmd.CommandText = "SELECT * FROM @Table; //設置操做語句 cmd.Parameters.Add("@Table", SqlDbType.String); //添加參數 cmd.Parameters["@Table"].Value = "Person"; //設置參數值
這麼作是想將一個字符串插入表名所在的位置,但只會獲得一個語法錯誤的提示。
真正生成到數據庫執行的SQL語句以下:
SELECT * FROM 'Person'
列名沒法做爲參數:
cmd.CommandText = "SELECT * FROM Person ORDER BY @Column; //設置操做語句 cmd.Parameters.Add("@Column", SqlDbType.String); //添加參數 cmd.Parameters["@Column"].Value = "Id"; //設置參數值
真正生成到數據庫執行的SQL語句以下:
SELECT * FROM Person ORDER BY 'Id'
SQL關鍵字不能做爲參數:
cmd.CommandText = "SELECT * FROM Person ORDER BY Id @Sort; //設置操做語句 cmd.Parameters.Add("@Sort", SqlDbType.String); //添加參數 cmd.Parameters["@Sort"].Value = "DESC"; //設置參數值
參數將被當作字面字符串插入而非SQL關鍵字。在這個例子中,會返回語法錯誤的提示:
SELECT * FROM Person ORDER BY Id 'DESC'
(3).存儲過程
存儲過程,是不少程序員生成能夠抵禦SQL注入攻擊的方法。一般來講,存儲過程包含固定的SQL語句,這些語句是在定義這個存儲過程的時候被解析的。
然而,存儲過程也是使用SQL動態查詢的,沒法絕對保證徹底杜絕SQL注入。不過存儲過程的確是有強大的防止SQL注入的做用。不過,若是你依然是在存儲過程當中拼接SQL語句,存儲過程也同樣可以注入。
假設你的存儲過程以下(這在比較複雜一些的操做時常常出現的):
CREATE PROC SelectAccount @name varchar(50), @password varchar(50) AS DECLARE @sql varchar(1000); SET @sql = 'SELECT * FROM Account WHERE Name = ''' + @name + ''' AND Password = ''' + @password + '''' EXEC (@sql)
若是在存儲過程中拼接SQL語句,那麼注入方式以下:
--正常登陸 EXECUTE SelectAccount 'admin','123456' --SQL注入,無需帳號行數不返回0 EXECUTE SelectAccount 'a'' or 1=1 --','';
返回結果:
事實上幾乎全部的數據庫應用程序都動態地構建SQL語句。若是你使用拼接字符串的形式或者將變量插入到字符串中的方法來構建哪怕一句SQL語句,那這一句查詢語句就會讓應用程序暴露在SQL注入攻擊的威脅之下。
沒有哪種技術能使SQL代碼變得安全,你應該學習下面所描述的全部技術,並在合理的地方使用它們。
一、過濾輸入內容
你應該將全部不合法的字符從用戶輸入中剔除掉,而不是糾結因而否有些輸入包含了有危險的內容。也就是說,若是你須要一個整數,那就只使用輸入中的整數部分。根據你所使用的開發語言不一樣,方法也不盡相同。
二、參數化動態內容
若是查詢中的變化部分是一些簡單的類型,你應該使用查詢參數將其和SQL表達式分離。
參數化動態內容以後,一個參數只能被替換成一個值。若是你是在RDMBS解析完SQL語句以後才插入這個參數值,沒有那種SQL注入的攻擊可以改變一個參數化了的查詢的語法結構。即便攻擊者嘗試使用帶有惡意的參數值,注入123 OR TRUE,RDBMS會將這個字符串當成一個完整的值插入。最壞的狀況下,這個查詢沒辦法返回任何記錄,它不會返回錯誤的行。
三、給動態輸入的值加引號
查詢參數一般來講是最好的解決方案,但在有些很特殊的狀況下,參數的佔位符會致使查詢優化器沒法正確選擇使用哪一個索引來進行優化。要規避參數查詢對索引的影響,直接將變量內容插入到SQL語句中會是更好的方法,不要去理會查詢參數。一旦你決定這麼作了,就必定要當心地引用字符串。你須要確信你插入的字符串是通過嚴格測試、不會帶有安全隱患的。
四、將用戶與代碼隔離
查詢參數和轉移字符能幫助你將字符串類型的值插入到SQL表達式中,但這些技術在須要插入表/列名或者SQL關鍵字的時候不起做用。你須要另外一項技術來使得這些部分也能動態化。
對應的解決方案是,將請求參數做爲索引值去查找預先定義好的值,而後用這些預先定義好的值來組織SQL查詢語句。
(1)、預約義值
如在一個數據字典中存儲以下值:
up => "ASC" ,down => "DESC"
(2)、定義默認值
定義一個默認值,當用戶選擇的值不在數據字典中時,使用默認值。
好比當用戶輸入的值不是up也不爲down,那麼就使用ASC。這樣的字符串就是安全的了。
下面給出一個簡單的SQL注入過濾函數:
public string NoSqlHack(string Inner) { if (!string.IsNullOrEmpty(Inner)) { //特殊的字符 Inner = Inner.Replace("<", ""); Inner = Inner.Replace(">", ""); Inner = Inner.Replace("*", ""); Inner = Inner.Replace("-", ""); Inner = Inner.Replace("?", ""); Inner = Inner.Replace("'", "''"); Inner = Inner.Replace(",", ""); Inner = Inner.Replace("/", ""); Inner = Inner.Replace(";", ""); Inner = Inner.Replace("*/", ""); Inner = Inner.Replace("\r\n", ""); Inner = Inner.Replace(" ", ""); return Inner; } else { return string.Empty; } }
另外,後端必定要驗證,以防止用戶POST請求。
SQL注入繞過某些字符過濾:
1,避免使用被阻止的字符,即不使用這些字符仍然達到攻擊目的。
A,若是注入一個數字數據字段,就不須要使用單引號。
B,輸入註釋符號被阻止使用,咱們能夠設計注入的數據,既不破壞周圍的查詢語法。
好比, http://www.xxx.net/article.asp?id=1' 這裏存在注入,過濾了註釋符合,咱們能夠輸入 http://www.xxx.net/article.asp?id=1' or 'a'='a
目的其實很簡單,就是把後面的單引號給閉合掉。
C,在一個MSSQL注入中注入批量查詢的時候,沒必要使用分號分隔符。
只要糾正全部批量查詢的語法,不管你是否使用分號,查詢的解析器依然能正確的去解釋它們的。
2,避免使用簡單確認
一些輸入確認機制使用一個簡單的黑名單,組織或刪除任何出如今這個名單中的數據,好比防注入程序。
這通常要看這個機制是否作的足夠的好了,黑名單是否足夠能確保安全。若是隻是簡單的黑名單,那也有機會突破的。
A,若是select關鍵詞被阻止或刪除
咱們能夠輸入:
SeLeCt 注意大小寫
selselectect 還記得ewebeditor是怎麼過濾asp的麼?
%53%45%4c%45%43%54 URL編碼
%2553%2545%254c%2545%2543%2554 對上面的每一個%後加了一個25
3,使用SQL註釋符
A,使用註釋來冒充注入的數據中的空格。
select/*alocne*/username,password/*alocne*/from/*alocne*/admin
/*alocne*/來冒充空格
B,使用註釋來避開某些注入的確認過濾。
SEL/*alocne*/ECT username,password fr/*alocne*/om admin
4,處理被阻止的字符串
好比,程序阻止了admin,由於怕攻擊者注入admin表單中的數據。
咱們能夠這樣
A,oracle數據庫: 'adm'||'in'
B,MSSQL數據庫: 'adm'+'in'
C,MYSQL數據庫: concat ('adm','in')
D,oracle中若是單引號被阻止了,還能夠用chr函數
sleect password from admin where username = char(97) || chr(100) || chr(109) || chr(105) || chr(110)