⚠️ 重要前言:現代 ASP.NET 應用首選參數化查詢 / ORM (EF/Dapper) 杜絕 SQL 注入。HTTP 模組屬於纵深防禦第二道防線,適用於:
public class SqlInjectModule : IHttpModule
{
private static readonly string[] _sqlKeywords;
private static readonly string _errorPage;
static SqlInjectModule()
{
// 從 web.config 讀取關鍵字與錯誤頁面
_sqlKeywords = (ConfigurationManager.AppSettings["SqlInjectKeywords"] ??
"and|exec|insert|select|delete|update|count|chr|mid|master|truncate|char|declare|drop|create|xp_|sp_|--|;|'|")
.Split('|', StringSplitOptions.RemoveEmptyEntries);
_errorPage = ConfigurationManager.AppSettings["SqlInjectErrorPage"] ?? "/Error/SqlInjectDetected.aspx";
}
public void Init(HttpApplication context)
{
// 使用 AcquireRequestState 確保 Session 已可用
context.AcquireRequestState += OnAcquireRequestState;
}
private void OnAcquireRequestState(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var request = app.Context.Request;
var response = app.Context.Response;
try
{
// 略過檔案上傳 (multipart/form-data)
if (IsMultipartContent(request)) return;
// 檢查 QueryString
CheckCollection(request.QueryString, "QueryString", request, response);
// 檢查 Form
CheckCollection(request.Form, "Form", request, response);
// 檢查 Cookies
CheckCookies(request.Cookies, request, response);
}
catch (Exception ex)
{
// 記錄錯誤但不中斷請求 (避免模組本身導致 500)
LogError(ex);
}
}
private bool IsMultipartContent(HttpRequest request)
{
string ct = request.ContentType ?? "";
return ct.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase);
}
private void CheckCollection(NameValueCollection collection, string source,
HttpRequest request, HttpResponse response)
{
if (collection == null) return;
foreach (string key in collection.Keys)
{
// 略過 ViewState
if (key.Equals("__VIEWSTATE", StringComparison.OrdinalIgnoreCase)) continue;
if (key.Equals("__EVENTVALIDATION", StringComparison.OrdinalIgnoreCase)) continue;
string value = collection[key];
if (ContainsSqlInjection(value))
{
LogAttack(source, key, value, request);
RedirectToErrorPage(response, $"{source} 參數 '{key}' 含疑似 SQL 注入字元");
}
}
}
private void CheckCookies(HttpCookieCollection cookies, HttpRequest request, HttpResponse response)
{
if (cookies == null) return;
foreach (string key in cookies.Keys)
{
var cookie = cookies[key];
if (cookie == null) continue;
if (ContainsSqlInjection(cookie.Value))
{
LogAttack("Cookie", key, cookie.Value, request);
RedirectToErrorPage(response, $"Cookie '{key}' 含疑似 SQL 注入字元");
}
}
}
private bool ContainsSqlInjection(string input)
{
if (string.IsNullOrEmpty(input)) return false;
string upperInput = input.ToUpperInvariant();
foreach (string keyword in _sqlKeywords)
{
if (upperInput.Contains(keyword.ToUpperInvariant()))
return true;
}
return false;
}
private void RedirectToErrorPage(HttpResponse response, string message)
{
try
{
response.Clear();
response.Redirect($"{_errorPage}?msg={HttpUtility.UrlEncode(message)}");
response.End();
}
catch { /* 忽略重導向錯誤 */ }
}
private void LogAttack(string source, string key, string value, HttpRequest request)
{
var log = new
{
Time = DateTime.Now,
Source = source,
Key = key,
Value = value.Length > 200 ? value.Substring(0, 200) + "..." : value,
IP = request.UserHostAddress,
Url = request.RawUrl,
UserAgent = request.UserAgent
};
// 寫入資料庫/檔案/ELK/Serilog
// Logger.Warn("SQL注入攔截: {@Log}", log);
}
private void LogError(Exception ex)
{
// Logger.Error(ex, "SQL注入模組內部錯誤");
}
public void Dispose() { }
}
<configuration>
<appSettings>
<!-- 危險關鍵字,以 | 分隔,不區分大小寫 -->
<add key="SqlInjectKeywords" value="and|exec|insert|select|delete|update|count|chr|mid|master|truncate|char|declare|drop|create|xp_|sp_|--|;|'|union|script|alert|onload|onerror|eval|expression" />
<add key="SqlInjectErrorPage" value="/Error/SqlInjectDetected.aspx" />
</appSettings>
<system.webServer>
<!-- IIS 7+ 整合模式 -->
<modules>
<add name="SqlInjectModule" type="YourNamespace.SqlInjectModule, YourAssembly" />
</modules>
</system.webServer>
<system.web>
<!-- IIS 6 / Classic 模式 -->
<httpModules>
<add name="SqlInjectModule" type="YourNamespace.SqlInjectModule, YourAssembly" />
</httpModules>
</system.web>
</configuration>
| 決策點 | 選擇 | 理由 |
|---|---|---|
| 事件選擇 | AcquireRequestState | 比 BeginRequest 晚,Session 已載入,可記錄用戶身分 |
| 關鍵字匹配 | ToUpper + Contains | 簡單、效能可接受;避免 Regex 複雜度與 ReDoS 風險 |
| 略過 ViewState | 硬編碼排除 | ViewState 含 Base64 經常觸發誤判 |
| 略過 multipart | ContentType 判斷 | 檔案二進位內容會大量誤判,且通常不經 SQL |
| 錯誤處理 | Try-Catch 包裹全流程 | 模組錯誤不應導致整站掛掉 |
關鍵字黑名單必然產生誤判 (如搜尋 "Android" 含 "and"、部落格內容含 "select")。緩解方案:
| 方案 | 優勢 | 適用場景 |
|---|---|---|
| WAF (Web Application Firewall) | 層級更高、規則庫更新、不侵入程式碼 | 生產環境必備 (Azure WAF, AWS WAF, ModSecurity, Cloudflare) |
| 參數化查詢 + 程式碼審查 | 根治源頭、零誤判、效能最佳 | 所有新開發專案 |
| ORM (EF Core / Dapper) | 強制參數化、編譯時檢查 | 現代 .NET 專案標配 |
| RASP (Runtime Application Self-Protection) | 運行時監控、零規則維護 | 高安全需求企業級應用 |
HTTP 模組 SQL 注入防護是維護舊系統的權宜之計,非長久之道。核心實作要點:AcquireRequestState 事件、排除 ViewState/multipart、關鍵字大小寫不敏感匹配、完整審計日誌、錯誤頁面友善導向。新專案請直接採用參數化查詢 + ORM + WAF 三層防護體系,將安全風險消除於開發階段。