首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> SQL Server 資料庫 >> 關於SqlServer

通用分頁存儲過程隱藏 SQL 注入風險,參數化也無法完全防禦

     編輯:關於SqlServer

前言:廣為流傳的分頁存儲過程其實暗藏殺機

在搜尋引擎輸入「分頁存儲過程」會跳出大量結果,許多開發團隊直接複製套用。然而,這類號稱「高效通用」的存儲過程卻存在嚴重的 SQL 注入漏洞,且無法單靠修改存儲過程本身來修補。許多人誤以為使用存儲過程就能天然防禦 SQL 注入,這在一般情況下成立,但對於動態拼接 SQL 字串的通用分頁存儲過程卻完全失效。本文將透過程式碼分析與實測,揭示漏洞成因並提供可行的防護方向。

典型通用分頁存儲過程的結構剖析

常見的通用分頁存儲過程通常接收表名、欄位、排序欄位、頁碼、頁大小、查詢條件(@strWhere)等參數,內部透過大量字串拼接組合出最終的 SQL 指令,再以 EXEC(@strSQL) 執行。以下簡化核心邏輯:

CREATE PROCEDURE Pagination
    @tblName      VARCHAR(255),
    @strGetFields VARCHAR(1000) = '*',
    @fldName      VARCHAR(255) = '',
    @PageSize     INT = 10,
    @PageIndex    INT = 1,
    @doCount      BIT = 0,
    @OrderType    BIT = 0,
    @strWhere     VARCHAR(1500) = ''  -- 注意:呼叫端不應加 WHERE 關鍵字
AS
BEGIN
    DECLARE @strSQL VARCHAR(5000), @strTmp VARCHAR(110), @strOrder VARCHAR(400);

    /* 總筆數統計 */
    IF @doCount != 0
    BEGIN
        SET @strSQL = 'SELECT COUNT(*) AS Total FROM [' + @tblName + ']' +
                      CASE WHEN @strWhere != '' THEN ' WHERE ' + @strWhere ELSE '' END;
    END
    ELSE
    BEGIN
        /* 排序方向 */
        IF @OrderType != 0
        BEGIN
            SET @strTmp = '<(SELECT MIN';
            SET @strOrder = ' ORDER BY [' + @fldName + '] DESC';
        END
        ELSE
        BEGIN
            SET @strTmp = '>(SELECT MAX';
            SET @strOrder = ' ORDER BY [' + @fldName + '] ASC';
        END

        /* 第一頁:直接 TOP N */
        IF @PageIndex = 1
        BEGIN
            SET @strSQL = 'SELECT TOP ' + CAST(@PageSize AS VARCHAR) + ' ' + @strGetFields +
                          ' FROM [' + @tblName + ']' +
                          CASE WHEN @strWhere != '' THEN ' WHERE ' + @strWhere ELSE '' END +
                          @strOrder;
        END
        /* 非第一頁:巢狀子查詢翻頁 */
        ELSE
        BEGIN
            SET @strSQL = 'SELECT TOP ' + CAST(@PageSize AS VARCHAR) + ' ' + @strGetFields +
                          ' FROM [' + @tblName + '] WHERE [' + @fldName + ']' + @strTmp +
                          '([' + @fldName + ']) FROM (SELECT TOP ' +
                          CAST((@PageIndex - 1) * @PageSize AS VARCHAR) + ' [' + @fldName + '] ' +
                          'FROM [' + @tblName + ']' + @strOrder + ') AS tblTmp' + @strOrder;
            IF @strWhere != ''
                SET @strSQL = @strSQL + ' AND ' + @strWhere;
        END
    END
    EXEC(@strSQL);
END

漏洞實測:單引號雙重轉義仍無法阻斷注入

假設前端使用 SqlParameter 傳遞 @strWhere,輸入使用者名稱 Jim's dog 進行模糊查詢。參數化後字串變為 UserName LIKE ''%Jim'' dog%''(單引號已雙化)。但存儲過程內部仍採用字串拼接:

SET @strSQL = 'SELECT TOP 20 * FROM [UserAccount] WHERE ' + @strWhere + ' ORDER BY ID DESC'

實際執行時,SQL 變為:

SELECT TOP 20 * FROM [UserAccount] WHERE UserName LIKE ''%Jim'' dog%'' ORDER BY ID DESC

在 SQL Server 中,'' 代表跳脫後的單引號,因此 LIKE ''%Jim'' 實際上只匹配 %Jim',後續 dog%'' 被視為語法一部份,導致語句截斷。若攻擊者輸入 Jim'; DELETE FROM UserAccount;--,經參數化雙化後仍能在拼接階段形成合法的多重陳述式,造成資料刪除或更嚴重的破壞。

為何參數化在存儲過程內部失效?

  • 參數化只在呼叫端生效:SqlParameter 確保傳入值不被視為 SQL 片段,但一旦進入存儲過程,@strWhere 只是普通字串變數。
  • 動態 SQL 拼接重新引入風險:EXEC(@strSQL) 將拼接後的字串當作新批次執行,此時已無參數化保護。
  • 存儲過程內無法再次參數化:SQL Server 不支援在動態 SQL 中對已拼接的字串進行二次參數化綁定。

可行的修復路徑

  1. 將拼接邏輯前移至資料存取層(DAL):在 C#/Java 等應用層組裝完整 SQL,並對所有使用者輸入使用參數化查詢,存儲過程僅負責固定邏輯(如 ROW_NUMBER() 翻頁)。
  2. 改用 SQL Server 2012+ 內建 OFFSET ... FETCH 分頁:避免動態 SQL,直接在參數化查詢中使用標準語法。
  3. 若必須保留存儲過程:將 @strWhere 拆解為結構化參數(欄位、運算子、值),在存儲過程內以 sp_executesql 搭配參數化執行,但會大幅增加複雜度,不建議。

總結

通用分頁存儲過程雖然寫法靈活,但其核心架構——「字串拼接 + EXEC 執行」——從根本上違背了參數化防禦原則。任何試圖在存儲過程內部修補的嘗試都治標不治本。唯有將動態 SQL 組裝移至應用層並全程參數化,或升級至支援原生分頁語法的資料庫版本,才能徹底消除此類注入風險。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved