首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET HttpModule 實作 SQL 注入防護與動態 JS/CSS 載入:雙重實用模組範例

     編輯:ASP.NET 基礎

模組一:SQL 注入防護 HttpModule (改良版)

針對舊系統無法全面重構參數化查詢時的緩解方案,採用 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() { }
}

模組二:動態載入 JS/CSS 統一管理模組

解決每個頁面重複引用公共腳本/樣式的問題,透過基類頁面 + 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;
    }
}

Web.config 整合雙模組


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

進階:ResourceInjectionModule 獨立實作


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 就緒)
測試性易單元測試需整合測試
效能目標頁面才執行所有受管請求執行 (略高開銷)
維護性需修改基類純配置/模組,零程式碼侵入

現代化替代:ASP.NET Core Middleware


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 體系,架構更清晰、效能更佳、支援依賴注入與單元測試。

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