16 Güvenlik Middleware'i: Sertleştirilmiş Bir .NET API Hattı İnşa Etmek
Diğer geliştiricilere .NET API'mizde 16 güvenlik middleware'i olduğunu söylediğimde, ilk tepki her zaman "bu biraz abartı değil mi?" olur. Sonra bir bankacılık uygulaması için finansal işlemleri işlediğimizi söylerim ve tepki "yalnızca 16 mı?"ya dönüşür. İşte hattımızdaki her middleware'in ayrıntılı bir dökümü, neden orada olduğu ve derinlemesine savunma (defense in depth) oluşturmak için nasıl birlikte çalıştıkları.
Hat Sırası Önemlidir
ASP.NET Core'da middleware yürütme sırası kritiktir. Bir istek, middleware'ler arasında yukarıdan aşağıya akar ve yanıt aşağıdan yukarıya geri akar. Sırayı yanlış yapmak yalnızca işlevselliği bozmaz — güvenlik açıkları yaratır.
csharp// Program.cs — The complete security pipeline
public void Configure(IApplicationBuilder app)
{
// 1. Request ID
app.UseMiddleware<RequestIdMiddleware>();
// 2. Security Headers
app.UseMiddleware<SecurityHeadersMiddleware>();
// 3. HTTPS Redirection
app.UseHttpsRedirection();
// 4. IP Whitelist (for admin endpoints)
app.UseMiddleware<IpWhitelistMiddleware>();
// 5. Rate Limiting
app.UseMiddleware<RateLimitingMiddleware>();
// 6. Request Size Limiting
app.UseMiddleware<RequestSizeLimitMiddleware>();
// 7. CORS
app.UseCors("BankingPolicy");
// 8. Request Logging (sanitized)
app.UseMiddleware<SanitizedRequestLoggingMiddleware>();
// 9. API Key Validation (service-to-service)
app.UseMiddleware<ApiKeyValidationMiddleware>();
// 10. Authentication
app.UseAuthentication();
// 11. JWT Custom Validation
app.UseMiddleware<JwtCustomValidationMiddleware>();
// 12. Authorization
app.UseAuthorization();
// 13. Session Validation
app.UseMiddleware<SessionValidationMiddleware>();
// 14. Anti-Replay
app.UseMiddleware<AntiReplayMiddleware>();
// 15. Input Sanitization
app.UseMiddleware<InputSanitizationMiddleware>();
// 16. Response Masking
app.UseMiddleware<ResponseMaskingMiddleware>();
app.MapControllers();
}
Her birini tek tek inceleyeyim.
1. Request ID Middleware
Her istek benzersiz bir korelasyon kimliği (correlation ID) alır. Bu, ilk sırada gelir çünkü sonraki her middleware bunu günlükleme (logging) için kullanır.
csharppublic class RequestIdMiddleware
{
private readonly RequestDelegate _next;
public RequestIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var requestId = context.Request.Headers["X-Request-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString("N");
context.Items["RequestId"] = requestId;
context.Response.Headers["X-Request-Id"] = requestId;
using (LogContext.PushProperty("RequestId", requestId))
{
await _next(context);
}
}
}
2. Güvenlik Başlıkları (Security Headers)
Güvenlik başlıklarını erkenden ayarlarız ki sonraki bir middleware kısa devre yapsa bile (short-circuit) mevcut olsunlar:
csharppublic class SecurityHeadersMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
context.Response.Headers["X-XSS-Protection"] = "0"; // Modern approach: rely on CSP
context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
context.Response.Headers["Content-Security-Policy"] =
"default-src 'self'; frame-ancestors 'none'";
context.Response.Headers["Permissions-Policy"] =
"camera=(), microphone=(), geolocation=()";
context.Response.Headers["Strict-Transport-Security"] =
"max-age=31536000; includeSubDomains; preload";
// Remove server identification
context.Response.Headers.Remove("Server");
context.Response.Headers.Remove("X-Powered-By");
await _next(context);
}
}
Not: X-XSS-Protection'ı bilerek 0 olarak ayarlıyoruz. Eski 1; mode=block değeri, eski tarayıcılarda aslında XSS açıkları getirebilir. Modern güvenlik, bunun yerine Content-Security-Policy'ye dayanır.
3-4. HTTPS Yönlendirmesi ve IP Beyaz Listesi
HTTPS yönlendirmesi yerleşiktir. IP beyaz listesi ise koşulludur — yalnızca yönetici (admin) uç noktalarına uygulanır:
csharppublic class IpWhitelistMiddleware
{
private readonly HashSet<string> _allowedIps;
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/api/admin"))
{
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
if (remoteIp == null || !_allowedIps.Contains(remoteIp))
{
_logger.LogWarning("Blocked admin access from IP {Ip}", remoteIp);
context.Response.StatusCode = 403;
return;
}
}
await _next(context);
}
}
5. Hız Sınırlama (Rate Limiting)
Uç nokta kategorisine göre farklı limitlerle kayan pencere (sliding window) algoritması kullanırız:
csharppublic class RateLimitingMiddleware
{
private readonly IDistributedCache _cache;
public async Task InvokeAsync(HttpContext context)
{
var clientId = GetClientIdentifier(context);
var endpoint = GetEndpointCategory(context.Request.Path);
var limit = endpoint switch
{
"auth" => new RateLimit(5, TimeSpan.FromMinutes(1)),
"transactions" => new RateLimit(30, TimeSpan.FromMinutes(1)),
"queries" => new RateLimit(100, TimeSpan.FromMinutes(1)),
_ => new RateLimit(60, TimeSpan.FromMinutes(1)),
};
var key = $"rate:{clientId}:{endpoint}";
var current = await _cache.GetAsync<SlidingWindow>(key);
if (current != null && current.RequestCount >= limit.MaxRequests)
{
context.Response.StatusCode = 429;
context.Response.Headers["Retry-After"] =
current.WindowResetSeconds.ToString();
return;
}
await IncrementWindow(key, limit);
await _next(context);
}
}
Kimlik doğrulama uç noktaları, kaba kuvvet (brute force) saldırılarını önlemek için dakikada 5 gibi katı bir limit alır. Sorgu uç noktaları ise dakikada 100 ile daha cömerttir.
6. İstek Boyutu Sınırlama
Finansal API'lerin büyük yükleri (payload) kabul etmesi gerekmez. İstek gövdelerini 1MB ile sınırlandırırız:
csharppublic class RequestSizeLimitMiddleware
{
private const long MaxRequestSize = 1_048_576; // 1 MB
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.ContentLength > MaxRequestSize)
{
context.Response.StatusCode = 413;
await context.Response.WriteAsJsonAsync(new
{
error = "Request body too large"
});
return;
}
await _next(context);
}
}
8. Temizlenmiş İstek Günlüğü (Sanitized Request Logging)
Bu middleware, denetim (audit) amaçlı istek ayrıntılarını günlüğe kaydederken PII'yi (kişisel olarak tanımlanabilir bilgi) ayıklar:
csharppublic class SanitizedRequestLoggingMiddleware
{
private static readonly HashSet<string> SensitiveFields = new()
{
"password", "token", "refreshToken", "ssn", "cardNumber",
"cvv", "pin", "accountNumber", "nationalId"
};
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
// Log sanitized request
if (context.Request.ContentType?.Contains("application/json") == true)
{
context.Request.EnableBuffering();
var body = await new StreamReader(context.Request.Body)
.ReadToEndAsync();
context.Request.Body.Position = 0;
var sanitized = SanitizeJson(body);
_logger.LogInformation(
"Request {Method} {Path} Body: {Body}",
context.Request.Method,
context.Request.Path,
sanitized);
}
await _next(context);
stopwatch.Stop();
_logger.LogInformation(
"Response {StatusCode} in {ElapsedMs}ms",
context.Response.StatusCode,
stopwatch.ElapsedMilliseconds);
}
private string SanitizeJson(string json)
{
var doc = JsonDocument.Parse(json);
return SanitizeElement(doc.RootElement).ToString();
}
}
11. JWT Özel Doğrulaması
Standart JWT doğrulamasının ötesinde, bankacılık bağlamımıza özgü özel iddiaları (custom claims) kontrol ederiz:
csharppublic class JwtCustomValidationMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated != true)
{
await _next(context);
return;
}
var claims = context.User.Claims;
// Verify device fingerprint matches
var tokenDeviceId = claims.FirstOrDefault(
c => c.Type == "device_id")?.Value;
var requestDeviceId = context.Request.Headers["X-Device-Id"]
.FirstOrDefault();
if (tokenDeviceId != null && tokenDeviceId != requestDeviceId)
{
_logger.LogWarning(
"Device mismatch: token={TokenDevice} request={RequestDevice}",
tokenDeviceId, requestDeviceId);
context.Response.StatusCode = 401;
return;
}
// Verify token hasn't been revoked
var jti = claims.FirstOrDefault(c => c.Type == "jti")?.Value;
if (jti != null && await _tokenRevocationService.IsRevoked(jti))
{
context.Response.StatusCode = 401;
return;
}
await _next(context);
}
}
13. Oturum Doğrulaması (Session Validation)
Bu, 8 saatlik sabit oturum zaman aşımımızı sunucu tarafında zorunlu kılar:
csharppublic class SessionValidationMiddleware
{
private static readonly TimeSpan SessionTimeout = TimeSpan.FromHours(8);
public async Task InvokeAsync(HttpContext context)
{
var sessionStart = context.User.Claims
.FirstOrDefault(c => c.Type == "session_start")?.Value;
if (sessionStart != null)
{
var startTime = DateTimeOffset.FromUnixTimeSeconds(
long.Parse(sessionStart));
if (DateTimeOffset.UtcNow - startTime > SessionTimeout)
{
context.Response.StatusCode = 401;
context.Response.Headers["X-Session-Expired"] = "true";
return;
}
}
await _next(context);
}
}
14. Anti-Replay
Her istek nonce'unun yalnızca bir kez kullanılmasını sağlayarak yeniden oynatma (replay) saldırılarını önler:
csharppublic class AntiReplayMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Method == "GET")
{
await _next(context);
return;
}
var nonce = context.Request.Headers["X-Request-Nonce"].FirstOrDefault();
var timestamp = context.Request.Headers["X-Request-Timestamp"]
.FirstOrDefault();
if (string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(timestamp))
{
context.Response.StatusCode = 400;
return;
}
// Reject requests older than 5 minutes
if (long.TryParse(timestamp, out var ts))
{
var requestTime = DateTimeOffset.FromUnixTimeSeconds(ts);
if (Math.Abs((DateTimeOffset.UtcNow - requestTime).TotalMinutes) > 5)
{
context.Response.StatusCode = 400;
return;
}
}
var key = $"nonce:{nonce}";
var exists = await _cache.GetAsync(key);
if (exists != null)
{
_logger.LogWarning("Replay attack detected. Nonce: {Nonce}", nonce);
context.Response.StatusCode = 409;
return;
}
await _cache.SetAsync(key, new byte[] { 1 },
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
await _next(context);
}
}
15-16. Girdi Temizleme (Input Sanitization) ve Yanıt Maskeleme (Response Masking)
Girdi temizleme, olası enjeksiyon vektörlerini ayıklar. Yanıt maskeleme ise tam hesap numaraları gibi hassas verilerin API yanıtlarında kısmen gizlenmesini (redact) sağlar:
csharppublic class ResponseMaskingMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
var originalBody = context.Response.Body;
using var newBody = new MemoryStream();
context.Response.Body = newBody;
await _next(context);
newBody.Seek(0, SeekOrigin.Begin);
var responseText = await new StreamReader(newBody).ReadToEndAsync();
if (context.Response.ContentType?.Contains("application/json") == true)
{
responseText = MaskSensitiveFields(responseText);
}
context.Response.Body = originalBody;
await context.Response.WriteAsync(responseText);
}
private string MaskSensitiveFields(string json)
{
// accountNumber: "1234567890" -> "******7890"
// email: "[email protected]" -> "u***@example.com"
// phone: "+905551234567" -> "+90*****4567"
// Implementation uses regex patterns per field type
}
}
Performans Etkisi
Doğal soru: 16 middleware'in performans maliyeti nedir? Her birini ölçtük:
| Middleware | Eklenen Ort. Gecikme | |-----------|------------------| | Request ID | <0,1ms | | Security Headers | <0,1ms | | IP Whitelist | <0,1ms | | Rate Limiting | ~1ms (Redis sorgusu) | | Request Size | <0,1ms | | Request Logging | ~2ms (temizleme dahil) | | JWT Custom Validation | ~1ms (iptal kontrolü) | | Session Validation | <0,1ms | | Anti-Replay | ~1ms (Redis sorgusu) | | Input Sanitization | ~1ms | | Response Masking | ~2ms | | Toplam ek yük | ~8-10ms |
İstek başına on milisaniye, derinlemesine savunma için seve seve ödeyeceğim bir bedel. Finansal bir uygulamada, tek bir güvenlik ihlali, tüm ürün yaşam döngüsünün kümülatif gecikme ek yükünden kat kat daha pahalıya mal olur.
Bu 16 middleware, güvenlik denetimlerinden, sızma testlerinden (penetration test) ve gerçek dünya olaylarından öğrenilen dersleri temsil ediyor. Her biri, hafiflettiği somut bir saldırı vektörü belirlediğimiz için var. Güvenlik kutucuk işaretlemek değildir — hiçbir tek başarısızlık noktasının sistemi tehlikeye atamayacağı katmanlı bir savunma meselesidir.