針對舊系統無法全面重構參數化查詢時的緩解方案,採用 AcquireRequestState 確保 Session 可用,排除 ViewState/multipart,支援白名單路徑排除。
public class SqlInjectionModule : IHttpModule
{
private static readonly HashSet _excludedPaths = new(StringComparer.OrdinalIgnoreCase);
private static readonly string[] _keywords;
private static readonly string _errorUrl;
static SqlInjectionModule()
{
_keywords = (ConfigurationManager.AppSettings["SqlInject:Keywords"] ??
"union select|exec |insert |delete |drop |create |truncate |declare |xp_|sp_|--|;--|'|script|alert|onload|onerror")
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
_errorUrl = ConfigurationManager.AppSettings["SqlInject:ErrorUrl"] ?? "/Error/SqlInject.aspx";
var excluded = ConfigurationManager.AppSettings["SqlInject:ExcludedPaths"] ?? "";
foreach (var p in excluded.Split(';', StringSplitOptions.RemoveEmptyEntries))
_excludedPaths.Add(p.Trim());
}
public void Init(HttpApplication context)
{
context.AcquireRequestState += OnAcquireRequestState;
}
private void OnAcquireRequestState(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var ctx = app.Context;
var request = ctx.Request;
var response = ctx.Response;
// 白名單路徑排除
if (_excludedPaths.Any(p => request.Path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
return;
// 略過檔案上傳
if (request.ContentType?.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase) == true)
return;
try
{
CheckCollection(request.QueryString, "QueryString", request);
CheckCollection(request.Form, "Form", request);
CheckCookies(request.Cookies, request);
}
catch (Exception ex)
{
// 記錄模組錯誤但不中斷請求
System.Diagnostics.Trace.TraceError($"SqlInjectionModule Error: {ex}");
}
}
private void CheckCollection(NameValueCollection col, string source, HttpRequest request)
{
if (col == null) return;
foreach (string key in col.Keys)
{
if (IsExcludedKey(key)) continue;
string value = col[key];
if (DetectInjection(value))
{
LogAttack(source, key, value, request);
ThrowAttackException(source, key);
}
}
}
private void CheckCookies(HttpCookieCollection cookies, HttpRequest request)
{
if (cookies == null) return;
foreach (string key in cookies.Keys)
{
var cookie = cookies[key];
if (cookie != null && DetectInjection(cookie.Value))
{
LogAttack("Cookie", key, cookie.Value, request);
ThrowAttackException("Cookie", key);
}
}
}
private bool IsExcludedKey(string key)
{
return key.Equals("__VIEWSTATE", StringComparison.OrdinalIgnoreCase) ||
key.Equals("__EVENTVALIDATION", StringComparison.OrdinalIgnoreCase) ||
key.Equals("__VIEWSTATEGENERATOR", StringComparison.OrdinalIgnoreCase);
}
private bool DetectInjection(string input)
{
if (string.IsNullOrEmpty(input)) return false;
string upper = input.ToUpperInvariant();
return _keywords.Any(k => upper.Contains(k.ToUpperInvariant()));
}
private void ThrowAttackException(string source, string key)
{
throw new HttpException(400, $"疑似 SQL 注入攻擊:{source} 參數 '{key}' 含危險字元");
}
private void LogAttack(string source, string key, string value, HttpRequest request)
{
var log = new
{
Time = DateTime.UtcNow,
Source = source,
Key = key,
Value = value.Length > 500 ? value.Substring(0, 500) : value,
IP = request.UserHostAddress,
Url = request.RawUrl,
UA = request.UserAgent
};
// 實際專案注入 ILogger 或寫入資料庫
System.Diagnostics.Trace.TraceWarning($"SQL Injection Blocked: {System.Text.Json.JsonSerializer.Serialize(log)}");
}
public void Dispose() { }
}
解決每個頁面重複引用公共腳本/樣式的問題,透過基類頁面 + HttpModule 實現自動注入。
public class BasePage : Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
InjectCommonResources();
}
private void InjectCommonResources()
{
string jsPath = "~/Scripts/Common/";
string cssPath = "~/Styles/Common/";
var jsFiles = GetResourceFiles(jsPath, "*.js");
var cssFiles = GetResourceFiles(cssPath, "*.css");
// 注入 CSS (放在 head 最前)
foreach (var css in cssFiles)
{
var link = new HtmlLink { Href = ResolveUrl(css), Attributes = { ["rel"] = "stylesheet", ["type"] = "text/css" } };
Header.Controls.AddAt(0, link);
}
// 注入 JS (放在 head 最後,確保依賴順序)
foreach (var js in jsFiles)
{
var script = new HtmlGenericControl("script");
script.Attributes["src"] = ResolveUrl(js);
script.Attributes["type"] = "text/javascript";
Header.Controls.Add(script);
}
}
private IEnumerable GetResourceFiles(string virtualPath, string pattern)
{
string physicalPath = Server.MapPath(virtualPath);
if (!Directory.Exists(physicalPath)) yield break;
foreach (var file in Directory.GetFiles(physicalPath, pattern, SearchOption.TopDirectoryOnly))
{
yield return VirtualPathUtility.Combine(virtualPath, Path.GetFileName(file));
}
}
private string ResolveUrl(string virtualPath)
{
if (virtualPath.StartsWith("~"))
return VirtualPathUtility.ToAbsolute(virtualPath);
return virtualPath;
}
}
<configuration>
<appSettings>
<add key="SqlInject:Keywords" value="union select|exec |insert |delete |drop |create |truncate |declare |xp_|sp_|--|;--|'|script|alert" />
<add key="SqlInject:ErrorUrl" value="/Error/SqlInject.aspx" />
<add key="SqlInject:ExcludedPaths" value="/Search/;/Admin/Editor/;/API/Public/" />
</appSettings>
<system.webServer>
<modules>
<add name="SqlInjectionModule" type="YourNamespace.SqlInjectionModule, YourAssembly" preCondition="managedHandler" />
<add name="ResourceInjectionModule" type="YourNamespace.ResourceInjectionModule, YourAssembly" preCondition="managedHandler" />
</modules>
</system.webServer>
</configuration>
public class ResourceInjectionModule : IHttpModule
{
private static readonly string[] _jsPaths = { "~/Scripts/Common/" };
private static readonly string[] _cssPaths = { "~/Styles/Common/" };
public void Init(HttpApplication context)
{
context.PostAcquireRequestState += OnPostAcquireRequestState;
}
private void OnPostAcquireRequestState(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var page = app.Context.CurrentHandler as Page;
if (page == null || page.Header == null) return;
// 避免重複注入
if (page.Items["ResourcesInjected"] != null) return;
page.Items["ResourcesInjected"] = true;
InjectResources(page);
}
private void InjectResources(Page page)
{
foreach (var cssPath in _cssPaths)
{
foreach (var file in GetFiles(page, cssPath, "*.css"))
{
var link = new HtmlLink { Href = file, Attributes = { ["rel"] = "stylesheet" } };
page.Header.Controls.AddAt(0, link);
}
}
foreach (var jsPath in _jsPaths)
{
foreach (var file in GetFiles(page, jsPath, "*.js"))
{
var script = new HtmlGenericControl("script");
script.Attributes["src"] = file;
script.Attributes["type"] = "text/javascript";
page.Header.Controls.Add(script);
}
}
}
private IEnumerable GetFiles(Page page, string virtualPath, string pattern)
{
string physical = page.Server.MapPath(virtualPath);
if (!Directory.Exists(physical)) yield break;
foreach (var f in Directory.GetFiles(physical, pattern))
yield return page.ResolveUrl(VirtualPathUtility.Combine(virtualPath, Path.GetFileName(f)));
}
public void Dispose() { }
}
| 特性 | BasePage 注入 | HttpModule 注入 |
|---|---|---|
| 適用範圍 | 繼承 BasePage 的頁面 | 所有 .aspx 頁面 (含不繼承基類) |
| 時機 | OnInit 階段 | PostAcquireRequestState (較晚,確保 Header 就緒) |
| 測試性 | 易單元測試 | 需整合測試 |
| 效能 | 目標頁面才執行 | 所有受管請求執行 (略高開銷) |
| 維護性 | 需修改基類 | 純配置/模組,零程式碼侵入 |
public class ResourceInjectionMiddleware
{
private readonly RequestDelegate _next;
private readonly IWebHostEnvironment _env;
public ResourceInjectionMiddleware(RequestDelegate next, IWebHostEnvironment env)
{
_next = next; _env = env;
}
public async Task InvokeAsync(HttpContext context)
{
// 僅處理 HTML 回應
if (context.Request.Path.Value.EndsWith(".html", StringComparison.OrdinalIgnoreCase) ||
context.Request.Path.Value.EndsWith("/", StringComparison.OrdinalIgnoreCase))
{
var originalBody = context.Response.Body;
using var buffer = new MemoryStream();
context.Response.Body = buffer;
await _next(context);
context.Response.Body = originalBody;
string html = Encoding.UTF8.GetString(buffer.ToArray());
// 注入資源到 前
string resources = GetResourceTags();
html = html.Replace("", resources + "");
await context.Response.WriteAsync(html);
}
else
{
await _next(context);
}
}
private string GetResourceTags()
{
// 掃描 wwwroot/Scripts/Common, wwwroot/Styles/Common 產生 link/script 標籤
return "";
}
}
HttpModule 提供橫切關注點的強大攔截點:SQL 注入防護作為舊系統纵深防禦,資源自動注入消除重複引用。關鍵實作細節:事件選擇 (AcquireRequestState/PostAcquireRequestState)、白名單排除、ViewState/multipart 略過、錯誤不中斷主流程、Header 控制項操作時機。現代 ASP.NET Core 應改用 Middleware + 標籤輔助器 + Bundler/Minifier 體系,架構更清晰、效能更佳、支援依賴注入與單元測試。