using System.Text.RegularExpressions; using System.Collections.Concurrent; using System.Text.Json; namespace DramaLing.Api.Middleware; /// /// 安全中間件,提供輸入驗證、速率限制和安全檢查 /// public class SecurityMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly SecurityOptions _options; // 簡單的記憶體速率限制器 private static readonly ConcurrentDictionary _rateLimits = new(); // 惡意模式檢測 private static readonly Regex[] SuspiciousPatterns = new[] { new Regex(@")<[^<]*)*<\/script>", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex(@"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDELETE\b|\bDROP\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex(@"(javascript:|data:|vbscript:)", RegexOptions.IgnoreCase | RegexOptions.Compiled), new Regex(@"(\.\./|\.\.\\)", RegexOptions.Compiled), // 路徑遍歷 new Regex(@"(eval\(|exec\(|system\()", RegexOptions.IgnoreCase | RegexOptions.Compiled) }; public SecurityMiddleware(RequestDelegate next, ILogger logger, SecurityOptions? options = null) { _next = next ?? throw new ArgumentNullException(nameof(next)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _options = options ?? new SecurityOptions(); } public async Task InvokeAsync(HttpContext context) { var clientId = GetClientIdentifier(context); var requestId = context.TraceIdentifier; try { // 1. 速率限制檢查 if (!await CheckRateLimitAsync(clientId, requestId)) { await RespondWithRateLimitExceeded(context); return; } // 2. 輸入安全驗證 if (!await ValidateInputSafetyAsync(context, requestId)) { await RespondWithSecurityViolation(context, "惡意輸入檢測"); return; } // 3. 請求大小檢查 if (!ValidateRequestSize(context)) { await RespondWithSecurityViolation(context, "請求大小超過限制"); return; } // 4. 新增安全標頭 AddSecurityHeaders(context); // 記錄安全事件 using var scope = _logger.BeginScope(new Dictionary { ["RequestId"] = requestId, ["ClientId"] = clientId, ["Method"] = context.Request.Method, ["Path"] = context.Request.Path, ["UserAgent"] = context.Request.Headers.UserAgent.ToString() }); await _next(context); } catch (Exception ex) { _logger.LogError(ex, "Security middleware error for request {RequestId}", requestId); throw; // 讓其他中間件處理異常 } } #region 速率限制 private Task CheckRateLimitAsync(string clientId, string requestId) { try { var now = DateTime.UtcNow; var clientLimit = _rateLimits.GetOrAdd(clientId, _ => new ClientRateLimit()); // 清理過期的請求記錄 clientLimit.Requests.RemoveAll(r => now - r > _options.RateLimitWindow); // 檢查是否超過速率限制 if (clientLimit.Requests.Count >= _options.MaxRequestsPerWindow) { _logger.LogWarning("Rate limit exceeded for client {ClientId}, request {RequestId}", clientId, requestId); return Task.FromResult(false); } // 記錄此次請求 clientLimit.Requests.Add(now); return Task.FromResult(true); } catch (Exception ex) { _logger.LogError(ex, "Error checking rate limit for client {ClientId}", clientId); return Task.FromResult(true); // 錯誤時允許通過,避免服務中斷 } } #endregion #region 輸入驗證 private async Task ValidateInputSafetyAsync(HttpContext context, string requestId) { try { if (context.Request.Method != "POST" && context.Request.Method != "PUT") { return true; // 只檢查可能包含輸入的請求 } var body = await ReadRequestBodyAsync(context); if (string.IsNullOrEmpty(body)) { return true; } // 檢查惡意模式 foreach (var pattern in SuspiciousPatterns) { if (pattern.IsMatch(body)) { _logger.LogWarning("Suspicious pattern detected in request {RequestId}: {Pattern}", requestId, pattern.ToString()); return false; } } // 檢查過長的輸入 if (body.Length > _options.MaxInputLength) { _logger.LogWarning("Input too long in request {RequestId}: {Length} characters", requestId, body.Length); return false; } return true; } catch (Exception ex) { _logger.LogError(ex, "Error validating input safety for request {RequestId}", requestId); return true; // 錯誤時允許通過 } } private async Task ReadRequestBodyAsync(HttpContext context) { try { context.Request.EnableBuffering(); using var reader = new StreamReader(context.Request.Body, leaveOpen: true); var body = await reader.ReadToEndAsync(); context.Request.Body.Position = 0; return body; } catch { return string.Empty; } } #endregion #region 請求大小驗證 private bool ValidateRequestSize(HttpContext context) { var contentLength = context.Request.ContentLength; if (contentLength.HasValue && contentLength.Value > _options.MaxRequestSize) { _logger.LogWarning("Request size {Size} exceeds limit {Limit} for {Path}", contentLength.Value, _options.MaxRequestSize, context.Request.Path); return false; } return true; } #endregion #region 安全標頭 private void AddSecurityHeaders(HttpContext context) { var response = context.Response; if (!response.Headers.ContainsKey("X-Content-Type-Options")) response.Headers.Append("X-Content-Type-Options", "nosniff"); if (!response.Headers.ContainsKey("X-Frame-Options")) response.Headers.Append("X-Frame-Options", "DENY"); if (!response.Headers.ContainsKey("X-XSS-Protection")) response.Headers.Append("X-XSS-Protection", "1; mode=block"); if (!response.Headers.ContainsKey("Referrer-Policy")) response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin"); } #endregion #region 輔助方法 private string GetClientIdentifier(HttpContext context) { // 使用 IP 地址作為客戶端識別 var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var userAgent = context.Request.Headers.UserAgent.ToString(); // 可以加入更複雜的指紋識別邏輯 return $"{ipAddress}_{userAgent.GetHashCode()}"; } private async Task RespondWithRateLimitExceeded(HttpContext context) { context.Response.StatusCode = 429; context.Response.ContentType = "application/json"; var response = new { Success = false, Error = new { Code = "RATE_LIMIT_EXCEEDED", Message = "請求過於頻繁,請稍後再試", RetryAfter = _options.RateLimitWindow.TotalSeconds }, Timestamp = DateTime.UtcNow }; var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); await context.Response.WriteAsync(json); } private async Task RespondWithSecurityViolation(HttpContext context, string reason) { context.Response.StatusCode = 400; context.Response.ContentType = "application/json"; var response = new { Success = false, Error = new { Code = "SECURITY_VIOLATION", Message = "安全檢查失敗", Reason = reason }, Timestamp = DateTime.UtcNow }; var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); await context.Response.WriteAsync(json); } #endregion } /// /// 安全中間件配置選項 /// public class SecurityOptions { /// /// 速率限制時間窗口 /// public TimeSpan RateLimitWindow { get; set; } = TimeSpan.FromMinutes(1); /// /// 時間窗口內最大請求數 /// public int MaxRequestsPerWindow { get; set; } = 60; /// /// 最大輸入長度 /// public int MaxInputLength { get; set; } = 10000; /// /// 最大請求大小(字節) /// public long MaxRequestSize { get; set; } = 1024 * 1024; // 1MB } /// /// 客戶端速率限制資訊 /// public class ClientRateLimit { public List Requests { get; set; } = new(); }