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
版權屬於:zgcwkj
本文鏈接:https://www.zgcwkj.com/archives/249.html
轉載聲明:請注明本文章的標題及內容的出處和聲明,謝謝
評論已關閉