Mert Özen Her Satırda Daha İleri
Güvenlik API Güvenliği (OWASP API Top 10) — #10

API Güvenliğini Sürekli Test Etmek ve İzlemek: Authorization Regresyonu, OpenAPI Tabanlı Tarama ve Abuse Telemetry

Mert Özen 8 Kas 2026 13 dk 2 görüntülenme
API Güvenliğini Sürekli Test Etmek ve İzlemek: Authorization Regresyonu, OpenAPI Tabanlı Tarama ve Abuse Telemetry

API güvenliğini sürekli korumak için kontrolleri bir test piramidine yerleştirir, OpenAPI tabanlı bir tarayıcıyı yalnız kendi ortamına karşı çalıştırıp bulgularını regresyon testine çevirir ve reddedilen istekleri tenant bazında ölçersin. Schemathesis'in bulduğu sözleşme farkları ve iki 500, RespectNullableAnnotations ile sistematik düzeltme, OpenTelemetry metric'leri ve tek sayfalık kontrol listesi.

Sürüm notu: Bu yazıdaki kodlar .NET 10, OpenTelemetry.Extensions.Hosting 1.20.1, Microsoft.Extensions.Diagnostics.Testing 10.11.0 ve Schemathesis 4.33.1 ile denenmiştir; tarama yalnız yerel lab'a karşı yapılmıştır.

Önce Tahmin Et

Dokuz yazı boyunca lab'a onlarca kontrol ekledik ve her birine bir test yazdık. Şimdi şu soruyu düşün: yarın biri POST /orders/cancel endpoint'ine {"orderIds": null} gönderirse ne olur? Testlerin hiçbiri bu girdiyi denemedi, çünkü biz test yazarken aklımıza gelen girdileri yazdık. Peki production'da bir tenant'ın başka tenant'ların sipariş id'lerini denediğini nereden anlarsın?

Bu yazı serinin kapanış yazısı gibi: kurduğumuz savunmaları bir test piramidine yerleştiriyor, OpenAPI dokümanından girdi üreten bir tarayıcıyı kendi lab'ımıza karşı çalıştırıp bulduklarını regresyon testine çeviriyor, reddedilen istekleri tenant bazında ölçen bir telemetry kuruyor ve en sonda tek sayfalık bir kontrol listesi çıkarıyoruz. Log'a neyin yazılıp neyin yazılmayacağının temeli Web Güvenliği (OWASP Top 10) serisinin A09 Security Logging and Alerting Failures yazısında.

Test Piramidi: Kim Neyi Yakalar

Lab'ın test projesinde şu an 186 test var ve hepsi yaklaşık 2,6 saniyede koşuyor. Dağılım piramidin şeklini gösteriyor:

  • Unit: OrderAuthorizationHandler'ın iptal kuralları, AdminPortFilter ve benzeri saf mantık. Host yok, veritabanı yok, milisaniyeler.
  • Integration: authorization matrisi (89 satır, generator'la 96 test), envanter testleri, property seviyesi, kaynak limitleri, hassas akışlar, dış istekler, yapılandırma. WebApplicationFactory ve in-memory SQLite ile.
  • Tarama: OpenAPI dokümanından otomatik girdi üreten bir araç, yalnız kendi ortamımıza karşı. Bizim aklımıza gelmeyen girdileri bulur.
  • Production telemetry: test değil ama aynı soruyu canlı trafikte soruyor: kim, nerede, ne sıklıkla reddediliyor?

Tabandaki unit testler pahalı kurulum istemeden kuralların bütün kombinasyonlarını denemeye yarıyor. İkinci yazıdaki iptal kuralının testi:

// Piramidin tabanı: handler'ın kuralları host olmadan, milisaniyeler içinde
public sealed class OrderAuthorizationHandlerTests
{
    private static async Task<bool> CanCancelAsync(string userName, string ownerName, OrderStatus status, string? tenant = null)
    {
        AppUser user = SeedData.UserNamed(userName);
        AppUser owner = SeedData.UserNamed(ownerName);
        ClaimsPrincipal principal = new(new ClaimsIdentity(
        [
            new Claim(LabClaims.Subject, user.Id.ToString()),
            new Claim(LabClaims.Tenant, tenant ?? user.TenantId),
            new Claim(LabClaims.Role, user.Role)
        ], "Test", LabClaims.Subject, LabClaims.Role));
        Order order = new() { Id = Guid.CreateVersion7(), TenantId = owner.TenantId, CustomerId = owner.Id,
            Status = status, ShippingAddress = "x" };
        AuthorizationHandlerContext context = new([OrderOperations.Cancel], principal, order);

        await new OrderAuthorizationHandler().HandleAsync(context);
        return context.HasSucceeded;
    }

    [Theory]
    [InlineData("alan", "alan", OrderStatus.Pending, true)]
    [InlineData("alan", "alan", OrderStatus.Paid, false)]
    [InlineData("alan", "grace", OrderStatus.Pending, false)]
    [InlineData("margaret", "alan", OrderStatus.Paid, true)]
    [InlineData("margaret", "alan", OrderStatus.Shipped, false)]
    [InlineData("ada", "alan", OrderStatus.Shipped, true)]
    [InlineData("ada", "alan", OrderStatus.Cancelled, false)]
    [InlineData("edsger", "alan", OrderStatus.Pending, false)]
    public async Task Cancel_rules(string user, string owner, OrderStatus status, bool expected)
    {
        Assert.Equal(expected, await CanCancelAsync(user, owner, status));
    }
}

Unit test kuralın doğru yazıldığını kanıtlıyor, ama kuralın doğru yerde çağrıldığını kanıtlamıyor. Handler'ı mükemmel test edip endpoint'e AuthorizeAsync çağrısını eklemeyi unutabilirsin. Bu yüzden piramidin ağırlığı integration katmanında: matris ve envanter testleri her endpoint'in gerçekten nasıl davrandığını soruyor. Authorization regresyonu dediğimiz şey de tam bu: dünkü kararın bugün de geçerli olduğunu her build'de kanıtlamak.

OpenAPI Tabanlı Tarama: Yalnız Kendi Ortamına

Matris, bizim düşündüğümüz senaryoları test ediyor. Düşünmediklerimiz için OpenAPI dokümanını okuyup her operasyona şemaya uygun ve uygun olmayan girdiler üreten bir araç kullanıyoruz. Lab'da Schemathesis'i (açık kaynak, şema tabanlı API (Application Programming Interface) test aracı) çalışma klasöründeki bir Python ortamına kurdum. OWASP ZAP'ın (Zed Attack Proxy) API scan modu da OpenAPI dokümanından aynı şekilde yola çıkıyor; ben bu yazıda yalnız Schemathesis'i çalıştırdım.

Önemli bir kural: bu araçlar yalnız sahibi olduğun ve test için ayrılmış bir ortama karşı çalıştırılır. Ben yerel lab'a, Development ortamında ve Margaret'ın token'ıyla çalıştırdım. CI'da (Continuous Integration) hedef kendi staging ortamın olmalı; production'a, bir iş ortağının sandbox'ına ya da bir üçüncü taraf API'sine asla. Tarama binlerce istek atar, veri oluşturur ve rate limit'leri tetikler.

$ schemathesis run http://localhost:19520/openapi/v1.json -H "Authorization: Bearer $MARGARET" --max-examples 15 --no-color

İlk koşunun özeti:

Failures:
  ❌ ST202 Schema-valid request rejected: 3
  ❌ ST103 Undocumented status code: 26

Errors:
  🚫 Schema Error: 1

Test cases:
  301 generated, 28 found 29 unique failures

Hiç sunucu hatası yok ama 26 operasyonun hepsinde "dokümante edilmemiş durum kodu" var. Sebep sözleşmede: dokümana göre her operasyon yalnız 200 dönüyor, oysa API 400, 401, 403, 404 ve 429 dönüyor. Üç operasyonda da "şemaya uygun istek reddedildi" bulgusu var: dokümana göre pageSize herhangi bir tam sayı olabilir, API ise 100'ün üstünü reddediyor. Tarayıcı bize sözleşme ile davranış arasındaki farkı gösterdi. Bu farkın güvenlik açısından anlamı şu: dokümana güvenerek istemci yazan bir ekip, limitleri ancak production'da öğrenir. Bir de ilginç ayrıntı: Development ortamında koştuğu için bazı 400 yanıtlarında exception ayrıntısı vardı. Taramayı production ayarlarındaki bir staging'e karşı yapmanın bir sebebi de bu; dokuzuncu yazıdaki API8 (Security Misconfiguration) testleri bunu ayrıca koruyor.

Sözleşmeyi iki adımda düzelttim. Bütün operasyonlara standart güvenlik yanıtlarını ekleyen bir OpenAPI operation transformer ve liste parametrelerine aralık bilgisi:

// Her operasyonun dönebileceği güvenlik yanıtları sözleşmeye yazılır; tarayıcı ve istemci bunları beklenmedik saymaz
public sealed class StandardResponsesTransformer : IOpenApiOperationTransformer
{
    private static readonly (string Code, string Description)[] Standard =
    [
        ("400", "The request is invalid."),
        ("401", "Authentication is required."),
        ("403", "The caller is not allowed to perform this operation."),
        ("404", "The resource does not exist or is outside the caller's scope."),
        ("429", "A rate limit or quota was exceeded.")
    ];

    public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context,
        CancellationToken cancellationToken)
    {
        operation.Responses ??= [];
        foreach ((string code, string description) in Standard)
        {
            operation.Responses.TryAdd(code, new OpenApiResponse { Description = description });
        }
        return Task.CompletedTask;
    }
}
public sealed record OrderListQuery(
    [property: Range(1, 10_000)] int? Page,
    [property: Range(1, ResourceLimits.MaxPageSize)] int? PageSize,
    [property: AllowedValues("createdAt", "-createdAt", "status", null)] string? Sort,
    OrderStatus? Status);

Range attribute'ları dokümana minimum ve maximum olarak yansıdı; AllowedValues ise yansımadı, dokümanda sort hâlâ serbest bir string. Bu attribute'lar burada yalnız sözleşme için: doğrulamayı endpoint'teki allowlist yapmaya devam ediyor.

Bulgudan Regresyon Testine

Sözleşme düzelince tarayıcı daha derine indi ve ikinci koşu asıl değerli bulguyu getirdi:

Failures:
  ❌ ST101 Server error: 2
  ❌ ST202 Schema-valid request rejected: 3
  ❌ ST103 Undocumented status code: 4
    curl -X POST -H 'Authorization: [Filtered]' -H 'Content-Type: application/json' -d '{"email": null}' http://localhost:19520/account/password-reset

    curl -X POST -H 'Authorization: [Filtered]' -H 'Content-Type: application/json' -d '{"orderIds": null}' http://localhost:19520/orders/cancel

İki endpoint de 500 döndü: biri NullReferenceException, diğeri Distinct() içinde ArgumentNullException. İki record'da da alanlar non-nullable olarak tanımlı (string Email, IReadOnlyList<Guid> OrderIds), ama System.Text.Json varsayılan olarak nullable annotation'larına bakmıyor ve null'ı kabul ediyor. Tek tek if (request.Email is null) eklemek bir sonraki endpoint'te unutulur. Sistematik düzeltme, JSON (JavaScript Object Notation) seçeneklerinde .NET 9 ile gelen iki ayarı açmak:

// Tarayıcının bulduğu iki 500'ün kök nedeni: non-nullable alana gelen null; artık binding'de 400
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.RespectNullableAnnotations = true;
    options.SerializerOptions.RespectRequiredConstructorParameters = true;
});

Her bulgu bir regresyon testine dönüşüyor, böylece tarayıcı bir daha koşmasa da hata geri gelmiyor:

// Schemathesis'in bulduğu 500'ler: her bulgu bir regresyon testine dönüşür
public sealed class ScanRegressionTests(OrdersApiFactory factory) : IClassFixture<OrdersApiFactory>
{
    [Theory]
    [InlineData("/account/password-reset", "anonymous", """{"email": null}""")]
    [InlineData("/orders/cancel", "margaret", """{"orderIds": null}""")]
    public async Task Null_for_a_non_nullable_field_is_a_400_not_a_500(string path, string actor, string body)
    {
        using HttpResponseMessage response = await factory.CreateClientFor(actor).PostAsync(path,
            new StringContent(body, Encoding.UTF8, "application/json"), TestContext.Current.CancellationToken);

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }
}

Üçüncü koşuda sunucu hatası kalmadı:

Failures:
  ❌ ST202 Schema-valid request rejected: 3
  ❌ ST103 Undocumented status code: 1

Errors:
  🚫 Schema Error: 1

Test cases:
  738 generated, 4 found 4 unique failures

Kalan bulgular bilinçli olarak kabul ettiklerim: şemada ifade edilemeyen çapraz kurallar (sayfa ile sayfa boyutunun çarpımı, rapor aralığının 92 günü geçmemesi), dokümana yansımayan sort allowlist'i ve dokuzuncu yazıda gördüğümüz, sürümlü route'taki {version} parametresinin dokümanda tanımlı olmaması. Bunları CI'da görmezden gelinecek bulgular olarak açıkça listelemek, yeni bir bulgunun gürültüde kaybolmasını engeller.

Abuse Telemetry: Ne Ölçülür

Testler bilinen davranışı korur; telemetry ise canlı trafikte olağan dışını gösterir. API güvenliği için en değerli sinyaller reddedilen istekler: 401 (kimlik yok ya da geçersiz), 403 (kimlik var, yetki yok), 404 (bazen kapsam dışı bir kayıt) ve 429 (limit). Bunları route, tenant ve durum koduna göre sayan küçük bir middleware ve ikinci yazıdaki ScopeProbe'a eklenen bir sayaç yazdım:

// Reddedilen istekleri route, tenant ve durum koduna göre sayar; kullanıcı id'si ya da IP metric tag'i olmaz
public sealed class SecurityMetrics
{
    public const string MeterName = "Bookstore.Orders.Security";
    private readonly Counter<long> _rejected;
    private readonly Counter<long> _outOfScope;

    public SecurityMetrics(IMeterFactory meterFactory)
    {
        Meter meter = meterFactory.Create(MeterName);
        _rejected = meter.CreateCounter<long>("bookstore.security.rejected_requests", "{request}",
            "Requests rejected with 401, 403, 404 or 429.");
        _outOfScope = meter.CreateCounter<long>("bookstore.security.out_of_scope_lookups", "{lookup}",
            "Lookups of records that exist outside the caller's scope.");
    }

    public void Rejected(string route, string tenant, int statusCode) =>
        _rejected.Add(1, new("http.route", route), new("tenant", tenant), new("http.response.status_code", statusCode));

    public void OutOfScope(string resource, string tenant) =>
        _outOfScope.Add(1, new("resource", resource), new("tenant", tenant));
}

Tag seçimi bilinçli. Route, tenant ve durum kodu sınırlı sayıda değer alır; metric sistemleri her tag kombinasyonu için ayrı bir seri tutar ve kullanıcı id'si ya da IP (Internet Protocol) adresi gibi sınırsız değerler hem maliyeti patlatır hem de kişisel veriyi metric altyapısına taşır. Kim olduğu sorusunun cevabı log'da, keyId ya da kullanıcı id'siyle; metric yalnız "nerede ve ne kadar" der. Eşleşmeyen yollar tek bir (unmatched) değerinde toplanıyor, yoksa tarama yapan biri her denediği URL (Uniform Resource Locator) için yeni bir seri yaratırdı.

ASP.NET Core'un kendi meter'ları da var: rate limiting için .NET 8'den beri, authorization için .NET 10 ile gelen sayaçlar. OpenTelemetry ile üçünü birlikte topluyorum. Lab'da console exporter'ı açıp Alan'la iki kapsam dışı ve bir hiç var olmayan sipariş, bir yasak endpoint, Barbara'yla da kotayı aşan üç rapor isteği attım:

Metric Name: bookstore.security.rejected_requests, Description: Requests rejected with 401, 403, 404 or 429., Unit: {request}, Metric Type: LongSum
Instrumentation scope (Meter):
	Name: Bookstore.Orders.Security
(2026-11-08T06:35:55.0891600Z, 2026-11-08T06:36:00.0301960Z] http.response.status_code: 404 http.route: /orders/{orderId:guid} tenant: north
Value: 3
(2026-11-08T06:35:55.0891600Z, 2026-11-08T06:36:00.0301960Z] http.response.status_code: 403 http.route: /support/orders/{orderId:guid} tenant: north
Value: 1
(2026-11-08T06:35:55.0891600Z, 2026-11-08T06:36:00.0301960Z] http.response.status_code: 429 http.route: /reports/daily-sales tenant: south
Value: 1

Metric Name: bookstore.security.out_of_scope_lookups, Description: Lookups of records that exist outside the caller's scope., Unit: {lookup}, Metric Type: LongSum
Instrumentation scope (Meter):
	Name: Bookstore.Orders.Security
(2026-11-08T06:35:55.0891870Z, 2026-11-08T06:36:00.0301980Z] resource: Order tenant: north
Value: 2

Metric Name: aspnetcore.authorization.attempts, Description: The total number of authorization attempts., Unit: {attempt}, Metric Type: LongSum
Instrumentation scope (Meter):
	Name: Microsoft.AspNetCore.Authorization
(2026-11-08T06:35:58.7851260Z, 2026-11-08T06:36:00.0301980Z] aspnetcore.authorization.result: success aspnetcore.user.is_authenticated: true
Value: 6
(2026-11-08T06:35:58.7851260Z, 2026-11-08T06:36:00.0301980Z] aspnetcore.authorization.result: failure aspnetcore.user.is_authenticated: true
Value: 1

Üç 404'ten ikisi out_of_scope_lookups'ta da görünüyor: bunlar var olan ama başka tenant'a ait kayıtlar. Üçüncüsü hiç var olmayan bir id. İki sayacın oranı alarm için iyi bir sinyal: bir tenant'ın 404'lerinin büyük kısmı kapsam dışı kayıtlara gidiyorsa biri id topluyordur ya da bir entegrasyon yanlış tenant'ın verisini istiyordur. Bu metric'ler de test ediliyor; MetricCollector<T> ile sayacın doğru tag'lerle arttığını kontrol eden bir test var.

Kurduğum alarmlar şu türden: bir tenant'ta 5 dakikadaki 403 sayısı son 7 günün aynı saatteki ortalamasının çok üstündeyse, out_of_scope_lookups sıfırdan farklıysa (normal trafikte neredeyse hiç olmamalı), bir route'taki 429 oranı birden artıyorsa. Mutlak eşik yerine tenant'ın kendi geçmişine göre sapma kullanmak, büyük ve küçük tenant'lar için aynı kuralı çalıştırmayı mümkün kılıyor.

Gateway ve WAF'ın Rolü ve Sınırı

API gateway ve WAF (Web Application Firewall) bu resmin neresinde? Gateway, bağlantı ve IP seviyesindeki limitler, TLS (Transport Layer Security) sonlandırma, kaba bir kimlik doğrulama ve route envanteri için doğru yer. WAF bilinen saldırı imzalarını, bozuk istekleri ve açık injection denemelerini kesmede iyi.

İkisinin de bilmediği şey senin iş mantığın. Bir WAF, Alan'ın GET /orders/0197f001-...-03 isteğinin neden reddedilmesi gerektiğini bilemez: istek sözdizimsel olarak kusursuz, token geçerli, id geçerli bir GUID (Globally Unique Identifier). Bu serinin ilk altı maddesi, yani BOLA (Broken Object Level Authorization), BOPLA (Broken Object Property Level Authorization), BFLA (Broken Function Level Authorization) ve hassas iş akışları, yalnız uygulamanın verisini bilen kodda çözülüyor. Gateway'i ilerideki API Gateway ve YARP (Yet Another Reverse Proxy) serisinde ayrıntılı kuracağız; buradaki pratik kural şu: gateway'deki her kontrol uygulamadakinin önüne eklenen bir katman, yerine geçen bir şey değil.

Tek Sayfalık API Güvenlik Kontrol Listesi

Serinin bütün kontrolleri tek tabloda. Her satır bir risk, lab'daki savunma ve onu koruyan test ya da kanıt:

RiskSavunmaTest ya da kanıt
API1 (BOLA)Named query filter ile tenant ve owner kapsamı, alt kaynakta ebeveyn koşulu, resource-based handlerAuthorization matrisi, başka tenant generator'ı, filtre ve IgnoreQueryFilters testleri
API2 (Broken Authentication)Prefix'li, hash'i saklanan, scope'lu API key; HMAC (Hash-based Message Authentication Code) imza; sıfırlama endpoint'inde IP ve hesap limitiApiKeyTests
API3 (BOPLA)Rol başına response tipi, Mapperly ile build kırılması, PATCH allowlist'iPropertyLevelTests, OpenAPI şema testi
API4 (kaynak tüketimi)Sayfa, offset, sort, toplu işlem, body ve aralık limitleri; timeout; plan bazlı kotaResourceLimitTests
API5 (BFLA)Açık yetki kararı, admin grubu, ayrı port ve gerçek port kontrolü, method override reddiEndpointInventoryTests, FunctionLevelTests
API6 (hassas akışlar)Idempotency-Key, kupon ve stok constraint'leri, step-up, akış limitiSensitiveFlowTests
API7 (SSRF: Server Side Request Forgery)Kayıtta URL biçimi, bağlantı anında adres kontrolü, redirect yokOutboundTests
API8 (yapılandırma)Production'da kapalı OpenAPI, ProblemDetails, dar CORS (Cross-Origin Resource Sharing), no-storeConfigurationTests
API9 (envanter)Build-time OpenAPI dosyası ve CI'da git diff, Deprecation ve SunsetEnvanter diff'i, deprecation testi
API10 (güvensiz tüketim)İmzalı ve doğrulanan webhook, şema ve aralık kontrolü, boyut limiti, resilienceOutboundTests, ScanRegressionTests

Üretimde Dikkat

Taramayı CI'da her pull request'te değil, staging'e deploy sonrası ve gecelik çalıştır; kısa bir koşu (operasyon başına birkaç örnek) her deploy'da, uzun bir koşu gece. Tarama hesabı kendi tenant'ında, sahte verilerle çalışmalı ve bu tenant'ın kotası bilinçli olarak ayarlanmalı; yoksa rate limit'ler taramanın büyük kısmını 429'a çevirir. Üçüncü koşuda Schemathesis'in "çoğunlukla 429 aldı" uyarısı tam bunu söylüyordu.

Telemetry'nin kendisi de bir saldırı yüzeyi. Metric ve log altyapısına giden veride token, API key, imza, e-posta ya da adres olmamalı. URL'in tamamını kaydeden bir tracing ya da access log yapılandırması query string'deki her şeyi de kaydeder; üçüncü yazıda API key'i query string'de reddetmemizin bir sebebi de buydu.

Alarmların bir sahibi olmalı. "403 oranı arttı" alarmını alan kişi ne yapacağını bilmiyorsa, alarm bir hafta sonra sessize alınır. Her alarm için kısa bir runbook yaz: hangi dashboard'a bakılır, hangi log sorgusu çalıştırılır, tenant'la kim konuşur.

Sık Düşülen Tuzaklar

Birinci tuzak, tarayıcı bulgusunu tek seferlik düzeltmek. Bulgu bir regresyon testine dönüşmezse bir sonraki refactor'da geri gelir ve tarayıcı tekrar koşana kadar fark edilmez.

İkincisi, tarama aracını kendine ait olmayan bir ortama karşı çalıştırmak. İş ortağının sandbox'ı, paylaşılan bir test ortamı ya da production tarama için uygun değil; hem hukuki hem operasyonel risk taşır.

Üçüncüsü, metric'e kullanıcı id'si, IP ya da ham URL koymak. Kardinalite patlar, maliyet artar ve kişisel veri log politikalarının dışında kalan bir sisteme sızar.

Dördüncüsü, gateway ya da WAF var diye uygulama içi yetkilendirmeyi gevşetmek. Gateway'den geçen her istek uygulamada da aynı kontrollerden geçmeli; gateway'i atlayan bir iç ağ çağrısı ya da yanlış bir route kuralı bu varsayımı bir gecede bozar.

Küçük Bir Deneme

★☆☆ Lab'ı Development'ta başlat ve Schemathesis'i Alan'ın token'ıyla çalıştır. Margaret'la aldığın sonuçtan ne farkı var ve neden?

★★☆ bookstore.security.rejected_requests için bir alarm kuralı tasarla: tenant başına 5 dakikalık pencere, son 7 günün aynı saatine göre sapma. Bunu bir metric sorgusu olarak yaz ve lab'da sahte trafikle tetikle.

★★★ Schemathesis'e bir yapılandırma dosyası ver: bilinen ve kabul edilen bulguları (ST202 çapraz kurallar, {version} şema hatası) açıkça hariç tutsun ve yeni bir bulgu çıktığında CI'ı kırsın. Kabul edilen bulguların listesini kod review'dan geçecek bir dosyada tut.

Serinin son yazısında geriye dönüp listeye bakıyoruz: OWASP API Security Top 10'un 2019 sürümünden 2023'e ne değişti, eski pentest raporlarını bugünün kategorilerine nasıl çeviririz ve yeni bir sürüm geldiğinde bu seriyi nasıl güncelleyeceğiz.