Blog'a Dön
.NETSecurityMiddlewareOWASPEnterprise

16 Güvenlik Middleware'i: Sertleştirilmiş Bir .NET API Hattı İnşa Etmek

Umut Korkmaz2025-04-1514 min read

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.

csharp
public 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:

csharp
public 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:

csharp
public 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:

csharp
public 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:

csharp
public 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:

csharp
public 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:

csharp
public 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:

csharp
public 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:

csharp
public 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:

csharp
public 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.