SQL 資料隱碼

適用於:SQL ServerAzure SQL DatabaseAzure SQL 受控執行個體Azure Synapse AnalyticsAnalytics Platform System (PDW)

SQL 插入式攻擊會將惡意程式碼插入字串,而此字串稍後會傳遞至 SQL Server 資料庫引擎執行個體以進行剖析和執行。 建構 SQL 陳述式的任何程序都應該經過檢閱,以確認有無插入弱點,因為資料庫引擎會執行收到的所有語法有效查詢。 即使是參數化資料也可能被技術純熟又執意操作的攻擊者操縱。

SQL 插入式攻擊如何運作

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--'

分號 (;) 表示結束一項查詢而開始另一項查詢。 而雙連字號 (--) 表示目前這一行的剩餘部分是註解,而且應該被忽略。 如果修改的程式碼語法正確,伺服器就會執行它。 資料庫引擎處理此陳述式時,它會先選取 OrdersTable 中的所有記錄,其中 ShipCityRedmond。 然後,資料庫引擎會捨棄 OrdersTable

只要插入的 SQL 程式碼語法正確,就無法以程式設計方式偵測竄改。 因此,您必須驗證所有使用者輸入,並且仔細檢視在您使用的伺服器中執行建構之 SQL 命令的程式碼。 這篇文章的下列各節描述編碼的最佳作法。

驗證所有輸入

透過測試類型、長度、格式和範圍,始終驗證使用者輸入。 當您針對惡意輸入實作一些預防措施時,請考慮您的應用程式的架構和部署狀況。 請記住,設計用在安全環境中執行的程式也可以複製至不安全的環境。 下列建議應視為最佳作法:

  • 對您的應用程式所接收之資料的大小、類型或內容不作任何假設。 例如,您應該做下列評估:

    • 如果錯誤或惡意的使用者在您的應用程式需要郵政編碼的地方,輸入 2 GB 的影片檔案,則應用程式會如何行為?

    • 如果在文字欄位中內嵌了 DROP TABLE 陳述式,則應用程式會執行什麼動作?

  • 測試輸入的大小和資料類型,並強制執行適當的限制。 這有助於防止蓄意的緩衝區滿溢。

  • 測試字串變數的內容,僅接受預期的值。 拒絕包含二進位資料、逸出序列和註解字元的項目。 這可有助於阻止指令碼資料隱碼,並可防止部分緩衝區滿溢嘗試。

  • 使用 XML 文件時,在輸入資料時即驗證所有資料的結構描述。

  • 絕不直接在使用者輸入中建立 Transact-SQL 陳述式。

  • 使用預存程序來驗證使用者輸入。

  • 在多層環境中,所有資料應在進入受信任區域之前進行驗證。 應拒絕未通過驗證處理的資料,且將錯誤傳回至上一層。

  • 實作多層驗證。 您針對隨意惡意使用者所採取的預防措施可能對執意操作的攻擊者無效。 較好的作法是在使用者介面中及以後每次跨越信任界限時都進行驗證。

    例如,用戶端應用程式的資料驗證可阻止簡單的指令碼資料隱碼。 然而,如果下一層假設其輸入已經過驗證,則能繞過用戶端的任何惡意使用者都可以不受限制地存取系統。

  • 絕不串連未經驗證的使用者輸入。 字串串連是指令碼資料隱碼的主要進入點。

  • 不要接受在可建構之檔案名稱的欄位中包含以下字串:AUXCLOCK$COM1COM8CONCONFIG$LPT1LPT8NULPRN

可能的話,請拒絕包含下列字元的輸入。

輸入字元 Transact-SQL 中的意義
; 查詢分隔符號。
' 字元資料字串分隔符號。
-- 單行註解分隔符號。 伺服器不會評估 -- 之後到該行結尾之前的文字。
/*** ... ***/ 註解分隔符號。 伺服器不會評估 /**/ 之間的文字。
xp_ 用在目錄擴充預存程序名稱的開頭,如 xp_cmdshell

使用類型安全的 SQL 參數

資料庫引擎中的 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 搭配使用 Parameters 集合

如果無法使用預存程序,您仍然可以使用參數,如下列程式碼範例所示。

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("_", "[_]");

檢閱 SQL 插入式攻擊的程式碼

您應該檢閱所有呼叫 EXECUTEEXECsp_executesql的程式碼。 您可以使用類似以下的查詢,來幫助您識別包含這些陳述式的程序。 這個查詢會檢查 EXECUTEEXEC這些字後面的 1、2、3 或 4 個空格。

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 若大於該變數已配置的緩衝區,就會被截斷。 能夠非預期傳遞長字串給預存程序來強制執行陳述式截斷的攻擊者,就可以操作此結果。 例如,以下範例預存程序容易受到截斷啟用的插入式攻擊。

在此範例中,我們有長度上限為 200 個字元的 @command 緩衝區。 我們需要總共 154 個字元來設定 'sa' 的密碼:26 個字元用於 UPDATE 陳述式、16 個字元用於 WHERE 子句、4 個字元用於 'sa'、2 個字元用於 QUOTENAME(@loginname) 括住的引號:200 - 26 - 16 - 4 - 2 = 154。 但是,因為 @new 宣告為 sysname,因此此變數只能保存 128 個字元。 我們可以藉由在 @new 中傳遞一些單引號來克服這一點。

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
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() 傳回的字串會自動被截斷。 下列範例所建立的預存程序會顯示可能發生的情況。

在此範例中,存放在臨時變數中的資料被截斷,因為 @login@oldpassword@newpassword 的緩衝區大小僅為 128 個字元,但 QUOTENAME() 最多可以傳回多達 258 個字元。 如果 @new 包含 128 個字元,則 @newpassword 可以是 123... n,其中 n 是第 127 個字元。 因為 QUOTENAME() 所傳回的字串會被截斷,所以可以讓它看起來像下列陳述式:

UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here

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);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
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() 時超出已配置的緩衝區來強制執行字串截斷。 下列範例所建立的預存程序會顯示可能發生的情況。

在此範例中,資料會被截斷,因為分派給 @login@oldpassword@newpassword 的緩衝區只能保留 128 個字元,但 QUOTENAME() 最多可傳回 258 個字元。 如果 @new 包含 128 個字元,@newpassword 可以是 '123...n',其中 n 是第 127 個字元。 因為 QUOTENAME() 所傳回的字串會被截斷,所以可以讓它看起來像下列陳述式:

UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here

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);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
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, ']') 時會截斷

當資料庫引擎安全性實體的名稱傳遞至使用 QUOTENAME(@variable, ']') 格式的陳述式時,會發生截斷。 下列範例示範此案例。

在此範例中,@objectname 必須允許 2 * 258 + 1 個字元。

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

當您串連 sysname 類型的值時,應使用夠大的暫存變數來保留每個值最多 128 個的字元。 可能的話,請直接在動態 Transact-SQL 內呼叫 QUOTENAME()。 否則,您可以計算必要的緩衝區大小,如上一節所述。