在搜尋引擎輸入「分頁存儲過程」會跳出大量結果,許多開發團隊直接複製套用。然而,這類號稱「高效通用」的存儲過程卻存在嚴重的 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 只是普通字串變數。EXEC(@strSQL) 將拼接後的字串當作新批次執行,此時已無參數化保護。ROW_NUMBER() 翻頁)。OFFSET ... FETCH 分頁:避免動態 SQL,直接在參數化查詢中使用標準語法。@strWhere 拆解為結構化參數(欄位、運算子、值),在存儲過程內以 sp_executesql 搭配參數化執行,但會大幅增加複雜度,不建議。通用分頁存儲過程雖然寫法靈活,但其核心架構——「字串拼接 + EXEC 執行」——從根本上違背了參數化防禦原則。任何試圖在存儲過程內部修補的嘗試都治標不治本。唯有將動態 SQL 組裝移至應用層並全程參數化,或升級至支援原生分頁語法的資料庫版本,才能徹底消除此類注入風險。