SQL 注入的主要形式包括直接將代碼插入到與 SQL 命令串聯在一起並使其得以執行的用戶輸入變量。一種間接的攻擊會將惡意代碼注入要在表中存儲或作爲元數據存儲的字符串。在存儲的字符串隨後串連到一個動態 SQL 命令中時,將執行該惡意代碼。
注入過程的工作方式是提前終止文本字符串,然後追加一個新的命令。由於插入的命令可能在執行前追加其他字符串,因此攻擊者將用註釋標記“--”來終止注入的字符串。執行時,此後的文本將被忽略。
以下腳本顯示了一個簡單的 SQL 注入。此腳本通過串聯硬編碼字符串和用戶輸入的字符串而生成一個 SQL 查詢:
var Shipcity; ShipCity = Request.form ("ShipCity"); var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";
用戶將被提示輸入一個市縣名稱。如果用戶輸入 Redmond,則查詢將由與下面內容相似的腳本組成:
SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond'
但是,假定用戶輸入以下內容:
Redmond'; drop table OrdersTable--
此時,腳本將組成以下查詢:
SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'
分號 (;) 表示一個查詢的結束和另一個查詢的開始。雙連字符 (--) 指示當前行餘下的部分是一個註釋,應該忽略。如果修改後的代碼語法正確,則服務器將執行該代碼。SQL Server 處理該語句時,SQL Server 將首先選擇OrdersTable 中的所有記錄(其中ShipCity 爲 Redmond)。然後,SQL Server 將刪除OrdersTable。
只要注入的 SQL 代碼語法正確,便無法採用編程方式來檢測篡改。因此,必須驗證所有用戶輸入,並仔細檢查在您所用的服務器中執行構造 SQL 命令的代碼。本主題中的以下各部分說明了編寫代碼的最佳做法。
始終通過測試類型、長度、格式和範圍來驗證用戶輸入。實現對惡意輸入的預防時,請注意應用程序的體系結構和部署方案。請記住,爲在安全環境下運行而設計的程序可能被複制到不安全的環境中。以下建議應被視爲最佳做法:
- 對應用程序接收的數據不做任何有關大小、類型或內容的假設。例如,您應該進行以下評估:
- 如果一個用戶在需要郵政編碼的位置無意中或惡意地輸入了一個 10 MB 的 MPEG 文件,應用程序會做出什麼反應?
- 如果在文本字段中嵌入了一個 DROP TABLE 語句,應用程序會做出什麼反應?
- 如果一個用戶在需要郵政編碼的位置無意中或惡意地輸入了一個 10 MB 的 MPEG 文件,應用程序會做出什麼反應?
- 測試輸入的大小和數據類型,強制執行適當的限制。這有助於防止有意造成的緩衝區溢出。
- 測試字符串變量的內容,只接受所需的值。拒絕包含二進制數據、轉義序列和註釋字符的輸入內容。這有助於防止腳本注入,防止某些緩衝區溢出攻擊。
- 使用 XML 文檔時,根據數據的架構對輸入的所有數據進行驗證。
- 絕不直接使用用戶輸入內容來生成 Transact-SQL 語句。
- 使用存儲過程來驗證用戶輸入。
- 在多層環境中,所有數據都應該在驗證之後才允許進入可信區域。未通過驗證過程的數據應被拒絕,並向前一層返回一個錯誤。
- 實現多層驗證。對無目的的惡意用戶採取的預防措施對堅定的攻擊者可能無效。更好的做法是在用戶界面和所有跨信任邊界的後續點上驗證輸入。
例如,在客戶端應用程序中驗證數據可以防止簡單的腳本注入。但是,如果下一層假設其輸入已被驗證,則任何可以跳過客戶端的惡意用戶就可能不受限制地訪問系統。 - 絕不串聯未驗證的用戶輸入。字符串串聯是腳本注入的主要輸入點。
- 在可能據以構造文件名的字段中,不接受下列字符串:AUX、CLOCK$、COM1 到 COM8、CON、CONFIG$、LPT1 到 LPT8、NUL 以及 PRN。
如果可能,拒絕包含以下字符的輸入。
輸入字符 | 在 Transact-SQL 中的含義 |
---|---|
; |
查詢分隔符。 |
' |
字符數據字符串分隔符。 |
-- |
註釋分隔符。 |
/* ... */ |
註釋分隔符。服務器不對 /* 和 */ 之間的註釋進行處理。 |
Xp_ |
用於目錄擴展存儲過程的名稱的開頭,如 xp_cmdshell。 |
使用類型安全的 SQL 參數
SQL Server 中的 Parameters 集合提供了類型檢查和長度驗證。如果使用 Parameters 集合,則輸入將被視爲文字值而不是可執行代碼。使用Parameters 集合的另一個好處是可以強制執行類型和長度檢查。範圍以外的值將觸發異常。以下代碼段顯示瞭如何使用Parameters 集合:
SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn); myCommand.SelectCommand.CommandType = CommandType.StoredProcedure; SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id", SqlDbType.VarChar, 11); parm.Value = Login.Text;
在此示例中,@au_id 參數被視爲文字值而不是可執行代碼。將對此值進行類型和長度檢查。如果 @au_id 值不符合指定的類型和長度約束,則將引發異常。
在存儲過程中使用參數化輸入
存儲過程如果使用未篩選的輸入,則可能容易受 SQL 注入攻擊。例如,以下代碼容易受到攻擊:
SqlDataAdapter myCommand = new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);
如果使用存儲過程,則應使用參數作爲存儲過程的輸入。
在動態 SQL 中使用參數集合
如果不能使用存儲過程,您仍可使用參數,如以下代碼示例所示:
SqlDataAdapter myCommand = new SqlDataAdapter( "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn); SQLParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id", SqlDbType.VarChar, 11); Parm.Value = Login.Text;
篩選輸入
篩選輸入可以刪除轉義符,這也可能有助於防止 SQL 注入。但由於可引起問題的字符數量很大,因此這並不是一種可靠的防護方法。以下示例可搜索字符串分隔符。
private string SafeSqlLiteral(string inputSQL) { return inputSQL.Replace("'", "''"); }
LIKE 子句
請注意,如果要使用 LIKE 子句,還必須對通配符字符進行轉義:
s = s.Replace("[", "[[]"); s = s.Replace("%", "[%]"); s = s.Replace("_", "[_]");
應檢查所有調用 EXECUTE、EXEC 或 sp_executesql 的代碼。可以使用類似如下的查詢來幫助您標識包含這些語句的過程。
SELECT object_Name(id) FROM syscomments
WHERE UPPER(text) LIKE '%EXECUTE (%'
OR UPPER(text) LIKE '%EXECUTE (%'
OR UPPER(text) LIKE '%EXECUTE (%'
OR UPPER(text) LIKE '%EXECUTE (%'
OR UPPER(text) LIKE '%EXEC (%'
OR UPPER(text) LIKE '%EXEC (%'
OR UPPER(text) LIKE '%EXEC (%'
OR UPPER(text) LIKE '%EXEC (%'
OR UPPER(text) LIKE '%SP_EXECUTESQL%'
使用 QUOTENAME() 和 REPLACE() 包裝參數
在選擇的每個存儲過程中,驗證是否對動態 Transact-SQL 中使用的所有變量都進行了正確處理。來自存儲過程的輸入參數的數據或從表中讀取的數據應包裝在 QUOTENAME() 或 REPLACE() 中。請記住,傳遞給 QUOTENAME() 的@variable 值的數據類型爲sysname,且最大長度爲 128 個字符。
@variable | 建議的包裝 |
---|---|
安全對象的名稱 |
QUOTENAME(@variable) |
字符串 ≤ 128 個字符 |
QUOTENAME(@variable, '''') |
字符串 > 128 個字符 |
REPLACE(@variable,'''', '''''') |
使用此方法時,可對 SET 語句進行如下修改:
--Before:
SET @temp = N'select * from authors where au_lname='''
+ @au_lname + N''''
--After:
SET @temp = N'select * from authors where au_lname='''
+ REPLACE(@au_lname,'''','''''') + N''''
由數據截斷啓用的注入
如果分配給變量的任何動態 Transact-SQL 比爲該變量分配的緩衝區大,那麼它將被截斷。如果攻擊者能夠通過將意外長度的字符串傳遞給存儲過程來強制執行語句截斷,則該攻擊者可以操作該結果。例如,以下腳本創建的存儲過程容易受到由截斷啓用的注入攻擊。
CREATE PROCEDURE sp_MySetPassword
@loginname sysname,
@old sysname,
@new sysname
AS
-- Declare variable.
-- Note that the buffer here is only 200 characters long.
DECLARE @command varchar(200)
-- Construct the dynamic Transact-SQL.
-- In the following statement, we need a total of 154 characters
-- to set the password of 'sa'.
-- 26 for UPDATE statement, 16 for WHERE clause, 4 for 'sa', and 2 for
-- quotation marks surrounded by QUOTENAME(@loginname):
-- 200 – 26 – 16 – 4 – 2 = 154.
-- But because @new is declared as a sysname, this variable can only hold
-- 128 characters.
-- We can overcome this by passing some single quotation marks in @new.
SET @command= 'update Users set password=' + QUOTENAME(@new, '''') + ' where username=' + QUOTENAME(@loginname, '''') + ' AND password = ' + QUOTENAME(@old, '''')
-- Execute the command.
EXEC (@command)
GO
通過向 128 個字符的緩衝區傳遞 154 個字符,攻擊者便可以在不知道舊密碼的情況下爲 sa 設置新密碼。
EXEC sp_MySetPassword 'sa', 'dummy', '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''
因此,應對命令變量使用較大的緩衝區,或直接在 EXECUTE 語句內執行動態 Transact-SQL。
使用 QUOTENAME(@variable, '''') 和 REPLACE() 時的截斷
如果 QUOTENAME() 和 REPLACE() 返回的字符串超過了分配的空間,該字符串將被自動截斷。以下示例中創建的存儲過程顯示了可能出現的情況。
CREATE PROCEDURE sp_MySetPassword
@loginname sysname,
@old sysname,
@new sysname
AS
-- Declare variables.
DECLARE @login sysname
DECLARE @newpassword sysname
DECLARE @oldpassword sysname
DECLARE @command varchar(2000)
-- In the following statements, the data stored in temp variables
-- will be truncated because the buffer size of @login, @oldpassword,
-- and @newpassword is only 128 characters, but QUOTENAME() can return
-- up to 258 characters.
SET @login = QUOTENAME(@loginname, '''')
SET @oldpassword = QUOTENAME(@old, '''')
SET @newpassword = QUOTENAME(@new, '''')
-- Construct the dynamic Transact-SQL.
-- If @new contains 128 characters, then @newpassword will be '123... n
-- where n is the 127th character.
-- Because the string returned by QUOTENAME() will be truncated,
-- it can be made to look like the following statement:
-- UPDATE Users SET password ='1234. . .[127] WHERE username=' -- other stuff here
SET @command = 'UPDATE Users set password = ' + @newpassword
+ ' where username =' + @login + ' AND password = ' + @oldpassword;
-- Execute the command.
EXEC (@command)
GO
因此,以下語句將把所有用戶的密碼都設置爲在前面的代碼中傳遞的值。
EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'
使用 REPLACE() 時,可以通過超出分配的緩衝區空間來強迫字符串截斷。以下示例中創建的存儲過程顯示了可能出現的情況。
CREATE PROCEDURE sp_MySetPassword
@loginname sysname,
@old sysname,
@new sysname
AS
-- Declare variables.
DECLARE @login sysname
DECLARE @newpassword sysname
DECLARE @oldpassword sysname
DECLARE @command varchar(2000)
-- In the following statements, data will be truncated because
-- the buffers allocated for @login, @oldpassword and @newpassword
-- can hold only 128 characters, but QUOTENAME() can return
-- up to 258 characters.
SET @login = REPLACE(@loginname, '''', '''''')
SET @oldpassword = REPLACE(@old, '''', '''''')
SET @newpassword = REPLACE(@new, '''', '''''')
-- Construct the dynamic Transact-SQL.
-- If @new contains 128 characters, @newpassword will be '123...n
-- where n is the 127th character.
-- Because the string returned by QUOTENAME() will be truncated, it
-- can be made to look like the following statement:
-- UPDATE Users SET password='1234…[127] WHERE username=' -- other stuff here
SET @command= 'update Users set password = ''' + @newpassword + ''' where username='''
+ @login + ''' AND password = ''' + @oldpassword + '''';
-- Execute the command.
EXEC (@command)
GO
與 QUOTENAME() 一樣,可以通過聲明對所有情況都足夠大的臨時變量來避免由 REPLACE() 引起的字符串截斷。應儘可能直接在動態 Transact-SQL 內調用 QUOTENAME() 或 REPLACE()。或者,也可以按如下方式計算所需的緩衝區大小。對於@outbuffer = QUOTENAME(@input),@outbuffer 的大小應爲2*(len(@input)+1).。使用 REPLACE() 和雙引號時(如上一示例),大小爲 2*len(@input) 的緩衝區便已足夠。
以下計算涵蓋所有情況:
While len(@find_string) > 0, required buffer size =
round(len(@input)/len(@find_string),0) * len(@new_string)
+ (len(@input) % len(@find_string))
使用 QUOTENAME(@variable, ']') 時的截斷
當 SQL Server 安全對象的名稱被傳遞給使用 QUOTENAME(@variable, ']') 形式的語句時,可能發生截斷。以下代碼顯示了這一可能性。
CREATE PROCEDURE sp_MyProc
@schemaname sysname,
@tablename sysname,
AS
-- Declare a variable as sysname. The variable will be 128 characters.
-- But @objectname actually must accommodate 2*258+1 characters.
DECLARE @objectname sysname
SET @objectname = QUOTENAME(@schemaname)+'.'+ QUOTENAME(@tablename)
-- Do some operations.
GO
當您串聯 sysname 類型的值時,應使用足夠大的臨時變量來保存每個值的最多 128 個字符。應儘可能直接在動態 Transact-SQL 內調用 QUOTENAME()。或者,也可以按上一部分所述來計算所需的緩衝區大小。