通過中間件實現簡易的 WAF 功能

@zgcwkj  2025年04月26日

分類:

代碼 網站 

C# MVC,通過中間件實現簡易的 WAF 功能

創建以下三個過濾器

WafSCFiter.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Text.RegularExpressions;

namespace WAFTest.Extensions
{
    /// <summary>
    /// WAF 安全檢查過濾器
    /// </summary>
    public class WafSCFiter : ActionFilterAttribute
    {
        /// <summary>
        /// 日志函數
        /// </summary>
        public LogFunction _LogFunc { get; }

        /// <summary>
        /// 網頁代碼函數
        /// </summary>
        private HtmlFunction _HtmlFunc { get; }

        /// <summary>
        /// 構造函數
        /// </summary>
        public WafSCFiter(LogFunction logFunc, HtmlFunction htmlFunc)
        {
            this._LogFunc = logFunc;
            this._HtmlFunc = htmlFunc;
        }

        /// <summary>
        /// 允許訪問前綴
        /// </summary>
        private static readonly List<string> _allowUrls = new()
        {
            "/api", // API接口
            "/verify", // 驗證碼
            "/wafverify", // 過驗證
            "/img",
            "/lib",
            "/css",
            "/js",
        };

        /// <summary>
        /// 允許訪問的IP
        /// </summary>
        private static readonly List<string> _allowIps = new()
        {
            //"::1",
            //"127.0.0.1",
        };

        /// <summary>
        /// 在執行前檢查
        /// </summary>
        /// <param name="context">執行上下文</param>
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var request = context.HttpContext.Request;
            var userIP = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
            var path = request.Path.ToString().ToLower();
            // 檢查過濾器狀態
            var isSkip = context.HttpContext.Items.TryGetValue("WAF_Skip", out var skipValue);
            if (isSkip && skipValue?.To<bool>() == true)
            {
                base.OnActionExecuting(context);
                return;
            }
            // 放行請求IP
            if (_allowIps.Any(w => w == userIP))
            {
                context.HttpContext.Items.Add("WAF_Skip", true);
                base.OnActionExecuting(context);
                return;
            }
            // 放行請求前綴
            if (_allowUrls.Any(w => path.StartsWith(w)))
            {
                context.HttpContext.Items.Add("WAF_Skip", true);
                base.OnActionExecuting(context);
                return;
            }
            // 檢查請求路徑是否包含敏感信息
            if (IsSensitivePath(request.Path))
            {
                var guid = Guid.NewGuid().ToString();
                _LogFunc.BaseLog("warn", "WAF", $"IP {userIP} 訪問 {request.Path} 參數包含惡意代碼({guid})");
                context.Result = GoBlocked(guid);
                return;
            }
            // 檢查URL查詢參數是否包含惡意代碼
            foreach (var query in request.Query)
            {
                if (ContainsSqlInjection(query.Value) || ContainsXSS(query.Value))
                {
                    var guid = Guid.NewGuid().ToString();
                    _LogFunc.BaseLog("warn", "WAF", $"IP {userIP} 訪問 {request.Path} 參數包含惡意代碼({guid})");
                    context.Result = GoBlocked(guid);
                    return;
                }
            }
            // 檢查表單數據是否包含惡意代碼
            if (request.HasFormContentType)
            {
                foreach (var form in request.Form)
                {
                    if (ContainsSqlInjection(form.Value) || ContainsXSS(form.Value))
                    {
                        var guid = Guid.NewGuid().ToString();
                        _LogFunc.BaseLog("warn", "WAF", $"IP {userIP} 訪問 {request.Path} 參數包含惡意代碼({guid})");
                        context.Result = GoBlocked(guid);
                        return;
                    }
                }
            }
            // 交給下一個過濾器處理
            base.OnActionExecuting(context);
        }

        /// <summary>
        /// SQL注入檢測模式,用於匹配常見的SQL注入攻擊模式
        /// </summary>
        private readonly string[] _sqlInjectionPatterns = new[]
        {
            @"\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|EXEC|ALTER|CREATE|TRUNCATE|DECLARE|WAITFOR|CAST|CONVERT)\b", // 常見SQL關鍵字
            @"[;]\s*(SELECT|INSERT|UPDATE|DELETE|DROP)", // 分號後跟SQL語句
            @"--[^\r\n]*", // SQL注釋
            @"/\*.*?\*/", // 多行注釋
            @"\band\b|\bor\b|\bxor\b|\bnot\b", // 邏輯運算符
            @"[+\-*/%%]\s*\d+\s*[=<>]", // 算術運算符
            @"\b(true|false)\b", // 布爾值
            @"'\s*[+\-*/%%]\s*'", // 字符串拼接
        };

        /// <summary>
        /// 檢查輸入是否包含SQL注入攻擊模式
        /// </summary>
        /// <param name="input">需要檢查的輸入字符串</param>
        /// <returns>如果包含SQL注入模式返回true,否則返回false</returns>
        private bool ContainsSqlInjection(string input)
        {
            if (string.IsNullOrEmpty(input)) return false;
            // URL解碼輸入
            var decodedInput = System.Web.HttpUtility.UrlDecode(input);
            // 規范化輸入(移除多餘空格,轉換為小寫)
            decodedInput = Regex.Replace(decodedInput, @"\s+", " ").Trim().ToLower();
            // 返回狀態
            return _sqlInjectionPatterns.Any(pattern => Regex.IsMatch(decodedInput, pattern, RegexOptions.IgnoreCase));
        }

        /// <summary>
        /// XSS攻擊檢測模式,用於匹配常見的跨站腳本攻擊模式
        /// </summary>
        private readonly string[] _xssPatterns = new[]
        {
            @"<script[^>]*>.*?</script>", // 腳本標簽
            @"javascript:", // JavaScript協議
            @"vbscript:", // VBScript協議
            @"onload=", // 加載事件
            @"onerror=", // 錯誤事件
        };

        /// <summary>
        /// 檢查輸入是否包含XSS攻擊模式
        /// </summary>
        /// <param name="input">需要檢查的輸入字符串</param>
        /// <returns>如果包含XSS攻擊模式返回true,否則返回false</returns>
        private bool ContainsXSS(string input)
        {
            if (string.IsNullOrEmpty(input)) return false;
            return _xssPatterns.Any(pattern => Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase));
        }

        /// <summary>
        /// 敏感路徑檢測模式,用於匹配可能包含敏感信息的URL路徑
        /// </summary>
        private readonly string[] _sensitivePathPatterns = new[]
        {
            @"\.config$", // 配置文件
            @"\.conf$", // 配置文件
            @"\.ini$", // INI配置文件
            @"\.env$", // 環境變量文件
            @"\badmin\b", // 管理員路徑
            @"\bmanage\b", // 管理路徑
        };

        /// <summary>
        /// 檢查請求路徑是否包含敏感信息
        /// </summary>
        /// <param name="path">需要檢查的請求路徑</param>
        /// <returns>如果是敏感路徑返回true,否則返回false</returns>
        private bool IsSensitivePath(PathString path)
        {
            var pathStr = path.ToString().ToLower();
            return _sensitivePathPatterns.Any(pattern => Regex.IsMatch(pathStr, pattern, RegexOptions.IgnoreCase));
        }

        /// <summary>
        /// 到攔截頁面
        /// </summary>
        /// <param name="id">攔截ID</param>
        /// <returns></returns>
        public ContentResult GoBlocked(string id)
        {
            var blockedHtml = @"
<!DOCTYPE html>

<html>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>安全攔截</title>
    <style>
        * { margin: 0;padding: 0; }
        body { width: 100vw;height: 100vh;background: #ffebee;display: flex;align-items: center;justify-content: center; }
        .content { height: 300px;display: flex;color: #f44336;align-items: center;flex-direction: column; }
        .content > svg { width: 80px;height: 80px;fill: #f44336; }
        .content > div { line-height: 50px;display: flex;align-items: center;flex-direction: column; }
    </style>
</head>
<body>
    <div class='content'>
        <svg viewBox='0 0 24 24'>
            <path d='M12 2L1 21h22L12 2zm0 3.45l8.27 14.32H3.73L12 5.45zm-1.5 8.09v-4h3v4h-3zm0 4h3v-2h-3v2z' />
        </svg>
        <div>
            <h1>訪問已被攔截</h1>
            <p>id: " + id + @"</p>
        </div>
    </div>
    <script>console.log('by zgcwkj')</script>
</body>
</html>";
            var result = new ContentResult
            {
                Content = _HtmlFunc.Compress(blockedHtml),
                ContentType = "text /html; charset=utf-8"
            };
            return result;
        }
    }
}
WafJSFiter.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;

namespace WAFTest.Extensions
{
    /// <summary>
    /// WAF 腳本檢查過濾器
    /// </summary>
    public class WafJSFiter : ActionFilterAttribute
    {
        /// <summary>
        /// 令牌存儲
        /// </summary>
        private static readonly ConcurrentDictionary<string, DateTime> _tokenStore = new();

        /// <summary>
        /// 安全功能
        /// </summary>
        private SecurityFunction _SecurityFunc { get; }

        /// <summary>
        /// 網頁代碼函數
        /// </summary>
        private HtmlFunction _HtmlFunc { get; }

        /// <summary>
        /// 構造函數
        /// </summary>
        public WafJSFiter(SecurityFunction securityFunc, HtmlFunction htmlFunc)
        {
            this._SecurityFunc = securityFunc;
            this._HtmlFunc = htmlFunc;
        }

        /// <summary>
        /// 在執行前檢查
        /// </summary>
        /// <param name="context">執行上下文</param>
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var request = context.HttpContext.Request;
            var response = context.HttpContext.Response;
            var userIP = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
            var userAgent = response.Headers.UserAgent;
            // 檢查過濾器狀態
            var isSkip = context.HttpContext.Items.TryGetValue("WAF_Skip", out var skipValue);
            if (isSkip && skipValue?.To<bool>() == true)
            {
                base.OnActionExecuting(context);
                return;
            }
            // 生成用戶唯一值
            var userIDKey = $"{userIP}_{userAgent}".ToMD5().ToLower();
            var userIDSalt = _SecurityFunc.Encrypt($"{DateTime.Now:yyyy-MM-dd}", userIDKey);
            // 檢查是否已通過驗證
            var wafVerifyKey = ".WafJsVerify.Cookies";
            var wafVerifyValue = request.Cookies[wafVerifyKey];
            if (wafVerifyValue != null)
            {
                // 驗證憑據是否存在且未過期
                if (_tokenStore.TryGetValue(wafVerifyValue, out var expirationTime) && DateTime.Now <= expirationTime)
                {
                    // 驗證憑據前綴
                    if (wafVerifyValue.Split('_')[0].Equals(userIDSalt))
                    {
                        base.OnActionExecuting(context);
                        return;
                    }
                }
                // 移除過期的憑據
                _tokenStore.TryRemove(wafVerifyValue, out _);
            }
            // 存儲憑據
            var verifyToken = GenerateVerifyToken();// 隨機驗證碼
            var verifyTokenExpiration = 30; // 有效期時間
            verifyToken = $"{userIDSalt}_{verifyToken}";// 增加驗證前綴
            _tokenStore[verifyToken] = DateTime.Now.AddMinutes(verifyTokenExpiration);
            // 驗證腳本
            var verifyCss = @"
.verify-wrapper {text-align: center;padding: 50px 20px;font-family: 'Microsoft YaHei', sans-serif;}
.verify-title {font-size: 24px;color: #333;margin-bottom: 20px;}
.verify-message {font-size: 16px;color: #666;margin-bottom: 30px;line-height: 1.6;}
.loading-spinner {display: inline-block;width: 40px;height: 40px;border: 4px solid #f3f3f3;border-top: 4px solid #3498db;border-radius: 50%;animation: spin 1s linear infinite;}
@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}
.verify-footer {text-align: center;}";
            var verifyJS = @"
(function() {
    try {
        // 基本JS執行驗證
        var container = document.getElementById('verify-container');
        if (!container) throw new Error('DOM操作失敗');
        // 數組操作驗證
        var arr = [1, 2, 3, 4, 5];
        var sum = arr.reduce((a, b) => a + b, 0);
        if (sum !== 15) throw new Error('數組操作失敗');
        // Promise驗證
        new Promise(resolve => resolve(true))
            .then(() => {
                // 設置驗證結果Cookie
                document.cookie = '" + wafVerifyKey + @"=;path=/;expires=Thu, 01 Jan 1970 00:00:00 UTC;';
                document.cookie = '" + wafVerifyKey + @"=' + encodeURIComponent('" + verifyToken + @"') + ';path=/;';
                // 驗證通過後刷新頁面
                setTimeout(() => window.location.reload(), 500);
            });
    } catch (err) {
        console.error('JS驗證失敗:', err);
    }
})();";
            var verifyHtml = $@"
<!DOCTYPE html>
<html>
<head>
    <title>安全驗證</title>
    <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <meta http-equiv='Cache-Control' content='no-cache'>
    <style>{verifyCss}</style>
</head>
<body>
    <div id='verify-container' style='display:none'></div>
    <div class='verify-wrapper'>
        <h1 class='verify-title'>安全驗證中</h1>
        <p class='verify-message'>系統正在進行安全驗證,請稍候...<br>驗證通過後將自動跳轉到目標頁面</p>
        <div class='loading-spinner'></div>
    </div>
    <hr />
    <div class='verify-footer'>
        <p><label>ID:</label>" + userIDKey + "<br><label>IP:</label>" + userIP + $@"</p>
    </div>
    <script>{_HtmlFunc.ObfuscatorJS(verifyJS)}</script>
</body>
</html>";
            // 注入驗證腳本
            context.Result = new ContentResult
            {
                Content = _HtmlFunc.Compress(verifyHtml),
                ContentType = "text/html; charset=utf-8"
            };
        }

        /// <summary>
        /// 生成驗證令牌
        /// </summary>
        private string GenerateVerifyToken()
        {
            var random = new Random();
            var token = random.Next(100000, 999999).ToString();
            using var sha256 = SHA256.Create();
            var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
            return Convert.ToBase64String(hashBytes);
        }
    }
}
WafCCFiter.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using System.Net;

namespace WAFTest.Extensions
{
    /// <summary>
    /// WAF 請求頻率過濾器
    /// </summary>
    public class WafCCFiter : ActionFilterAttribute
    {
        /// <summary>
        /// 訪問白名單
        /// <para>Key為IP地址,Value為過期時間</para>
        /// </summary>
        private static readonly Dictionary<string, DateTime> _whiteList = new();

        /// <summary>
        /// 日志函數
        /// </summary>
        public LogFunction _LogFunc { get; }

        /// <summary>
        /// 內存緩存
        /// </summary>
        private IMemoryCache _IMemoryCache { get; }

        /// <summary>
        /// 安全功能
        /// </summary>
        private SecurityFunction _SecurityFunc { get; }

        /// <summary>
        /// 網頁代碼函數
        /// </summary>
        private HtmlFunction _HtmlFunc { get; }

        /// <summary>
        /// 實例化
        /// </summary>
        public WafCCFiter(LogFunction logFunc, IMemoryCache memoryCache, SecurityFunction securityFunc, HtmlFunction htmlFunc)
        {
            this._LogFunc = logFunc;
            this._IMemoryCache = memoryCache;
            this._SecurityFunc = securityFunc;
            this._HtmlFunc = htmlFunc;
        }

        /// <summary>
        /// 在執行前檢查
        /// </summary>
        /// <param name="context">執行上下文</param>
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var request = context.HttpContext.Request;
            var userIP = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
            var path = request.Path.ToString().ToLower();
            // 檢查過濾器狀態
            var isSkip = context.HttpContext.Items.TryGetValue("WAF_Skip", out var skipValue);
            if (isSkip && skipValue?.To<bool>() == true)
            {
                base.OnActionExecuting(context);
                return;
            }
            // 檢查IP是否在白名單中且未過期
            if (_whiteList.TryGetValue(userIP, out var expireTime) && expireTime > DateTime.Now)
            {
                base.OnActionExecuting(context);
                return;
            }
            // 如果IP已過期,從白名單中移除
            else if (_whiteList.ContainsKey(userIP))
            {
                _whiteList.Remove(userIP);
            }
            // 檢查請求頻率
            var cacheKey = $"CC_FILTER_{userIP}";
            var requestInfo = _IMemoryCache.GetOrCreate(cacheKey, entry =>
            {
                entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
                return new RequestInfo { Count = 0, FirstRequestTime = DateTime.Now };
            })!;
            // 請求頻率規則
            var random = new Random();
            var maxRequestsMinute = 1; // 1分鍾內
            //var maxRequestsPer = random.Next(50, 100); // 50至100次請求
            var maxRequestsPer = random.Next(10, 30);
            // 加滿計數器
            if (requestInfo.Count > maxRequestsPer)
            {
                requestInfo.Count = int.MaxValue;
            }
            // 重置計數器
            else if ((DateTime.Now - requestInfo.FirstRequestTime).TotalMinutes >= maxRequestsMinute)
            {
                requestInfo.Count = 1;
                requestInfo.FirstRequestTime = DateTime.Now;
            }
            // 增加計數器
            else
            {
                requestInfo.Count++;
            }
            // 檢查是否超過訪問限制
            _IMemoryCache.Set(cacheKey, requestInfo);
            if (requestInfo.Count > maxRequestsPer)
            {
                _LogFunc.BaseLog("warn", "WAFCC", $"IP {userIP} 訪問頻率過高,已被攔截");
                context.Result = GoVerify(userIP);
                return;
            }
            // 交給下一個過濾器處理
            base.OnActionExecuting(context);
        }

        /// <summary>
        /// 請求信息
        /// </summary>
        private class RequestInfo
        {
            public int Count { get; set; }
            public DateTime FirstRequestTime { get; set; }
        }

        /// <summary>
        /// 到驗證頁面
        /// </summary>
        /// <param name="ip">請求IP</param>
        /// <returns></returns>
        public ContentResult GoVerify(string ip)
        {
            var code = _SecurityFunc.Encrypt(ip);
            var blockedHtml = @"
<!DOCTYPE html>

<html>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>安全驗證</title>
    <style>
        * { margin: 0;padding: 0; }
        body { width: 100vw;height: 100vh;background: #ffebee;display: flex;align-items: center;justify-content: center; }
        .content { height: 300px;display: flex;color: #f44336;align-items: center;flex-direction: column; }
        .content > svg { width: 80px;height: 80px;fill: #f44336; }
        .content > div { line-height: 50px;display: flex;align-items: center;flex-direction: column; }
        .verify-container { display: flex;align-items: center;gap: 10px;margin: 10px 0; }
        #verify { padding: 8px 12px;border: 2px solid #f44336;border-radius: 4px;outline: none;font-size: 16px; }
        #verify:focus { border-color: #d32f2f;box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.2); }
        #verifyImg { height: 40px;border-radius: 4px;cursor: pointer; }
        button { padding: 8px 24px;background: #f44336;color: white;border: none;border-radius: 4px;font-size: 16px;cursor: pointer; }
        button:hover { background: #d32f2f; }
    </style>
</head>
<body>
    <div class='content'>
        <svg class='logo' viewBox='0 0 24 24'>
            <path d='M12 2L1 21h22L12 2zm0 3.45l8.27 14.32H3.73L12 5.45zm-1.5 8.09v-4h3v4h-3zm0 4h3v-2h-3v2z' />
        </svg>
        <div>
            <h1>請完成驗證</h1>
            <h3>檢測到您的訪問頻率較高,需要進行人機驗證。</h3>
            <div class='verify-container'>
                <input id='verify' placeholder='請輸入驗證碼'>
                <img id='verifyImg' src='/Verify?code=" + code + @"'>
            </div>
            <button id='submit'>提交</button>
            <p>ip: " + ip + @"</p>
        <div>
    </div>
    <script>
        // 刷新驗證碼
        let verifyImg = document.querySelector('#verifyImg');
        verifyImg.addEventListener('click', function () {
            this.src = verifyImg.src + '&t=' + Math.random();
        });
        // 提交驗證碼
        let verify = document.querySelector('#verify');
        let submit = document.querySelector('#submit');
        submit.addEventListener('click', async function () {
            if (verify.value.length == 0) {
                alert('請輸入驗證碼');
                return;
            }
            // 提交驗證
            try {
                const formData = new FormData();
                formData.append('code', '" + code + @"');
                formData.append('verify', verify.value);
                const response = await fetch('/WafVerify', {
                    method: 'POST',
                    body: formData
                }).then(res => {
                    if (!res.ok) throw new Error('請求失敗');
                    return res.text();
                });
                // 驗證結果
                let result = JSON.parse(response);
                if (result.data === true) {
                    window.location.reload();
                } else {
                    alert('驗證失敗');
                }
            } catch (error) {
                alert(error.message);
            }
        });
    </script>
</body>
</html>";
            var result = new ContentResult
            {
                Content = _HtmlFunc.Compress(blockedHtml),
                ContentType = "text /html; charset=utf-8"
            };
            return result;
        }

        #region 對外函數

        /// <summary>
        /// 添加IP到白名單
        /// </summary>
        /// <param name="ip">IP地址</param>
        /// <param name="expireMinutes">過期時間(分鍾),默認5分鍾</param>
        public static void AddToWhiteList(string ip, int expireMinutes = 5)
        {
            if (IPAddress.TryParse(ip, out _))
            {
                _whiteList[ip] = DateTime.Now.AddMinutes(expireMinutes);
            }
        }

        /// <summary>
        /// 從白名單移除IP
        /// </summary>
        /// <param name="ip">IP地址</param>
        public static void RemoveFromWhiteList(string ip)
        {
            _whiteList.Remove(ip);
        }

        #endregion 對外函數
    }
}

啟用過濾器

//添加 過濾器
builder.Services.AddControllers(options =>
{
    options.Filters.Add<WafSCFiter>();//安全檢查
    options.Filters.Add<WafJSFiter>();//腳本檢查
    options.Filters.Add<WafCCFiter>();//訪問頻率檢查
});

完整示例源碼

WAFTest(NetCode):

內容已隱藏,需要評論並且審核通過後,才能閱讀隱藏內容

WAFRunJS(GoLang):WAFRunJS



評論已關閉

Top