Skip to main content
Kaynak Kod Analizi · 56 dk okuma

.NET'te Yetkilendirme Hatalarını Kaynak Koddan Tespit Etme

ASP.NET Core projesinde [Authorize] görmek, yetkilendirmenin doğru uygulandığını kanıtlamaz. Etkili kaynak kod incelemesi endpoint metadata’sından policy ve handler mantığına, tenant filtrelerinden background consumer’lara kadar aynı işleme ulaşan bütün yolları birlikte değerlendirir.

SX

SECNODEX Güvenlik Ekibi

Ofansif Güvenlik

Bir ASP.NET Core API düşünün. Controller üzerinde [Authorize] var. JWT doğrulaması çalışıyor. Kullanıcı giriş yapmadan endpoint’e erişemiyor. Testlerde 401 ve 403 response’ları da beklenen şekilde dönüyor.

İlk bakışta yetkilendirme tamamlanmış görünüyor.

Fakat endpoint aşağıdaki kodla faturayı getiriyor:

[Authorize]
[HttpGet("{invoiceId:guid}")]
public async Task<ActionResult<InvoiceDto>> GetInvoice(Guid invoiceId)
{
    var invoice = await db.Invoices
        .SingleOrDefaultAsync(x => x.Id == invoiceId);

    return invoice is null
        ? NotFound()
        : Ok(mapper.Map<InvoiceDto>(invoice));
}

Bu action yalnızca authenticated kullanıcıya açıktır. Buna karşılık herhangi bir authenticated kullanıcı, geçerli bir invoiceId bildiğinde başka müşterinin veya başka tenant’ın faturasını okuyabilir. [Authorize] doğru çalışmış, authentication başarılı olmuş, framework beklenen policy’yi uygulamıştır. Güvenlik açığı yine de vardır.

Sorun şudur:

Not

Endpoint’e girme izni ile o endpoint üzerinden seçilen object üzerinde belirli bir operation gerçekleştirme izni aynı karar değildir.

.NET yetkilendirme hatalarının önemli bir bölümü [Authorize] eksikliğinden değil, korumanın yanlış seviyede kurulmasından doğar. Kullanıcı sisteme girebilir ama bu faturayı okuyabilir mi? Manager role’üne sahiptir ama kendi oluşturduğu ödemeyi onaylayabilir mi? Tenant administrator’ıdır ama request ile gelen tenantId üzerinde mi, token’daki tenant üzerinde mi işlem yapmalıdır? Refund policy’sini karşılar ama bu siparişin mevcut state’i refund’a izin veriyor mu?

Bu soruların cevabı attribute’ta değil, endpoint metadata’sı, policy registration, authorization handler, query, domain service ve veri modeli arasındaki ilişkidedir.

Bu yazıda .NET yetkilendirme incelemesini gerçek bir Secure Code Review çalışması gibi ele alacağız. DefaultPolicy ve FallbackPolicy farkından [AllowAnonymous] precedence davranışına, role ve policy birleşim mantığından IAuthorizationService kullanımına, Minimal API route group’larından Razor Pages, SignalR ve gRPC entry point’lerine kadar authorization kararının kaynak koddan nasıl izlendiğini inceleyeceğiz.

1. .NET'te yetkilendirme hatası nedir?

Yetkilendirme hatası, bir actor’ın sahip olmadığı bir operation’ı gerçekleştirebilmesi veya erişmemesi gereken resource’a ulaşabilmesidir. Actor son kullanıcı, administrator, servis hesabı, API client, background worker veya başka bir sistem olabilir. Resource ise tek bir database row’dan rapora, tenant’a, dosyaya, queue message’a veya yönetim function’ına kadar geniş bir alanı kapsar.

Bu tanım dört soruya ayrılabilir:

  1. 1Actor kim? Kararı hangi identity, client veya service adına veriyoruz?
  2. 2Resource hangisi? Erişim kararı tam olarak hangi object, tenant veya veri kümesi için veriliyor?
  3. 3Operation nedir? Read, create, update, delete, approve, export, refund veya impersonate işlemlerinden hangisi isteniyor?
  4. 4Context nedir? Resource’un state’i, actor ile ilişkisi, zaman, kanal, risk seviyesi veya iş kuralı kararı değiştiriyor mu?

Bir kaynak kod incelemesinde bu dört unsurdan biri belirsizse yalnızca attribute kontrolüyle güvenilir sonuca ulaşılamaz.

Authentication ile authorization aynı şey değildir

Authentication, request’i yapan subject’in kimliğini doğrular. Authorization ise doğrulanmış subject’in talep ettiği işlemi yapıp yapamayacağını belirler.

JWT’nin signature’ının doğrulanması, issuer, audience ve expiration kontrollerinin geçmesi authentication sonucudur. Bu sonuç kullanıcının kim olduğunu ve token’ın hangi authority tarafından üretildiğini gösterebilir. Kullanıcının invoice/42 kaydını görebileceğini kendiliğinden kanıtlamaz.

Bu ayrım kod incelemesinde önemlidir. Aşağıdaki sinyaller authorization kanıtı değildir:

  • Endpoint’e yalnızca geçerli token ile girilebilmesi
  • User.Identity.IsAuthenticated değerinin true olması
  • DTO içinde userId veya tenantId bulunması
  • UI’da butonun düşük yetkili kullanıcıya gösterilmemesi
  • Repository’nin yalnızca primary key ile doğru kaydı bulması
  • Kullanıcının genel bir User, Manager veya Admin role’üne sahip olması
  • Frontend’in izin verilmeyen request’i göndermemesi

Authorization server-side verilecek, talep edilen resource ve operation ile bağlanacak bir karardır.

Horizontal, vertical ve context-dependent authorization

.NET kaynak kod analizi sırasında yetki hatalarını üç temel gruba ayırmak yararlıdır:

Yetki hatasıSoruÖrnek
Horizontal privilege escalationAynı yetki seviyesindeki başka actor’ın resource’una erişilebilir mi?Müşteri başka müşterinin faturasını okuyor
Vertical privilege escalationDüşük yetkili actor daha yüksek yetkili operation yapabiliyor mu?Support kullanıcısı administrator export’unu çağırıyor
Context-dependent authorizationNormalde izinli operation yanlış state veya ilişki içinde yapılabiliyor mu?Manager kendi oluşturduğu transferi onaylıyor

IDOR ve API dünyasındaki BOLA çoğunlukla object-level authorization eksikliğinin sonucudur. Ancak her authorization açığı yalnızca object identifier değiştirilerek ortaya çıkmaz. Toplu export, bulk update, field-level değişiklik, workflow transition, tenant seçimi ve privileged background action da aynı inceleme modeline alınmalıdır.

IDOR açığının neden hâlâ bu kadar yaygın olduğunu ele aldığımız yazıda identifier’ın kendisinden çok eksik object-level authorization kararının root cause olduğunu ayrıntılı biçimde açıklıyoruz.

2. Kaynak kod incelemesi [Authorize] aramakla başlamamalıdır

[Authorize] için repository genelinde arama yapmak faydalıdır. Fakat yalnızca korunan endpoint’leri gösterir. Asıl risk, korunması gerekip de metadata almayan endpoint’lerde, AllowAnonymous ile açılan yollarda ve HTTP dışı caller’larda olabilir.

İnceleme önce uygulamanın authorization surface’ini çıkarmalıdır.

SAST, manuel inceleme ve framework’e özgü review adımlarının aynı çalışma içinde nasıl konumlandırılması gerektiğini Kaynak Kod Analizi (SAST) Nedir? Kapsamlı Rehber sayfasında bütünlüklü olarak ele alıyoruz.

Entry point envanteri

ASP.NET Core projesinde yalnızca controller action’ları değil, en az şu entry point’ler aranmalıdır:

  • MVC ve API controller action’ları
  • Minimal API MapGet, MapPost, MapPut, MapPatch, MapDelete kayıtları
  • MapGroup ile oluşturulan route group’ları
  • Razor Pages ve area’lar
  • SignalR hub ve hub method’ları
  • gRPC service ve method’ları
  • GraphQL query ve mutation resolver’ları
  • Background service ve scheduled job’lar
  • Message queue consumer’ları
  • Webhook receiver’lar
  • Management ve health endpoint’leri
  • File download, preview ve export function’ları
  • Admin panel ve support tooling endpoint’leri
  • Internal olarak etiketlenmiş fakat network üzerinden erişilebilir API’ler

Bir domain operation’a yalnızca controller’dan ulaşıldığını varsaymak tehlikelidir. Örneğin RefundAsync hem controller, hem payment webhook, hem de retry consumer tarafından çağrılabilir. Controller üzerindeki policy diğer iki caller’a taşınmaz.

Authorization control envanteri

Ardından karar üreten bütün mekanizmalar çıkarılmalıdır:

  • [Authorize] ve [AllowAnonymous]
  • RequireAuthorization() ve AllowAnonymous()
  • AddPolicy, RequireRole, RequireClaim, RequireAssertion
  • DefaultPolicy ve FallbackPolicy
  • Custom IAuthorizationPolicyProvider
  • IAuthorizationRequirement
  • AuthorizationHandler<TRequirement>
  • AuthorizationHandler<TRequirement, TResource>
  • IAuthorizationService.AuthorizeAsync
  • User.IsInRole, User.HasClaim ve doğrudan ClaimsPrincipal kontrolleri
  • MVC authorization filter’ları
  • Endpoint filter ve custom middleware’ler
  • Repository tenant filter’ları ve EF Core global query filter’ları
  • Domain service içindeki ownership, approval ve state kontrolleri

Bu envanter iki farklı soruyu cevaplar:

  1. 1Hangi entry point hangi authorization metadata’sını alıyor?
  2. 2Aynı business operation’a ulaşan bütün yollar aynı security invariant’ı enforce ediyor mu?

İlk tarama için pratik code search

Aşağıdaki komutlar finding üretmez. Reviewer’a inceleme haritası verir:

rg -n --glob '*.cs' \
  '\[(Authorize|AllowAnonymous)|RequireAuthorization|AllowAnonymous\(' .

rg -n --glob '*.cs' \
  'AddPolicy|FallbackPolicy|DefaultPolicy|RequireRole|RequireClaim|RequireAssertion' .

rg -n --glob '*.cs' \
  'IAuthorizationService|AuthorizeAsync|AuthorizationHandler|IAuthorizationRequirement' .

rg -n --glob '*.cs' \
  'Map(Get|Post|Put|Patch|Delete)|MapGroup|MapHub|MapGrpcService' .

rg -n --glob '*.cs' \
  'User\.IsInRole|User\.HasClaim|FindFirst|FindFirstValue|ClaimsPrincipal' .

rg -n --glob '*.cs' \
  'FindAsync|FindById|SingleOrDefaultAsync|FirstOrDefaultAsync|IgnoreQueryFilters' .

Bu sonuçlar build graph ve route registration ile birlikte okunmalıdır. Extension method içine taşınmış endpoint’ler, partial class’lar, source-generated kod ve ayrı project’lerdeki handler registration’ları gözden kaçabilir.

Negative space neden önemlidir?

İyi review yalnızca var olan kontrolleri değil, bulunmayan kontrolleri de arar.

Örneğin repository genelinde 60 action’ın 48’inde [Authorize], 7’sinde [AllowAnonymous] olabilir. Kalan 5 action otomatik olarak zafiyet değildir. Fakat bunların public olma gerekçesi kanıtlanmalıdır. Uygulama güvenliği “çoğu endpoint korunmuş” oranıyla değerlendirilmez. Tek bir unprotected export veya administration endpoint’i bütün modeli bozabilir.

Bu nedenle envanter şu sütunlarla tutulabilir:

Entry pointAuthenticationCoarse policyResource checkTenant sourceOperationPublic gerekçesi
GET /api/invoices/{id}BearerCustomerEksikToken claimReadUygulanamaz
POST /api/refundsBearerRefundOperatorOrderPolicyServer contextRefundUygulanamaz
POST /webhooks/paymentSignatureYokEvent-order checkPayload mappingConfirmProvider callback

Bu tablo yalnızca documentation değildir. Aynı operation’a giden caller’lar arasındaki kontrol farkını görünür hâle getirir.

3. DefaultPolicy, FallbackPolicy ve endpoint metadata birlikte okunmalıdır

ASP.NET Core authorization review’ın en sık karıştırılan alanlarından biri DefaultPolicy ile FallbackPolicy farkıdır.

  • DefaultPolicy, policy adı belirtilmeden kullanılan [Authorize] veya RequireAuthorization() için devreye girer.
  • FallbackPolicy, endpoint üzerinde authorization metadata’sı bulunmadığında uygulanır.
  • [AllowAnonymous] veya .AllowAnonymous() ilgili endpoint’te authorization check’lerini bypass eder.

Bu üç davranış birbirinin alternatifi değildir.

Güvenli varsayılan: authenticated-by-default

Yeni endpoint’in yanlışlıkla public kalma riskini azaltmak için fallback policy kullanılabilir:

using Microsoft.AspNetCore.Authorization;

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

Bu yaklaşım, authorization metadata’sı bulunmayan endpoint’lerde authenticated user gerektirir. Public olması gereken endpoint’ler bilinçli şekilde [AllowAnonymous] veya .AllowAnonymous() ile işaretlenir.

Fakat fallback policy bir object-level authorization çözümü değildir. Bütün endpoint’leri authenticated yapmak, kullanıcıların yalnızca kendi resource’larına erişeceğini göstermez.

Default policy’nin zayıflatılması

Aşağıdaki configuration teknik olarak geçerlidir fakat beklenenden daha geniş erişim üretebilir:

builder.Services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAssertion(_ => true)
        .Build();
});

Projede reviewer yalnızca controller’lardaki [Authorize] attribute’larını görürse bütün bu action’ların korunduğunu düşünebilir. Oysa policy adı olmayan [Authorize], bu zayıf default policy’yi kullanır.

Bu nedenle aşağıdakiler birlikte incelenmelidir:

  • AddAuthorization ve AddAuthorizationBuilder registration’ları
  • Environment veya configuration’a göre değişen policy registration
  • Test project’lerinde override edilen authorization service’leri
  • Custom IAuthorizationPolicyProvider
  • DefaultPolicy ve FallbackPolicy
  • Policy name constant’ları ve string literal’lar
  • Endpoint’e uygulanmış bütün metadata

Fallback policy’nin kapsamı yanlış yorumlanabilir

Fallback policy yalnızca “attribute unutulduğunda koruma” sağlar. Endpoint üzerinde başka authorization metadata’sı varsa fallback yerine o metadata değerlendirilir. Bu ayrım özellikle özel policy’lerde önemlidir:

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .RequireRole("Employee")
        .Build();

    options.AddPolicy("ExternalPartner", policy =>
        policy.RequireClaim("partner_id"));
});

[Authorize(Policy = "ExternalPartner")] kullanılan endpoint’in fallback policy’deki Employee requirement’ını da otomatik olarak alacağı varsayılmamalıdır. İstenen karar hem partner claim’i hem belirli authentication şartlarıysa bunlar named policy içinde açıkça tanımlanmalıdır.

Middleware order gerçekten incelenmelidir

Klasik endpoint routing kullanılan uygulamada UseAuthorization, UseRouting sonrasında ve endpoint execution öncesinde yer almalıdır:

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

UseAuthorization routing öncesinde çalışırsa endpoint metadata’sına göre doğru policy’yi seçemez. Microsoft’un ASP0001 analyzer kuralı bu yanlış sıralamayı işaretler.

Minimal hosting modelinde WebApplication, gerekli service’ler kayıtlı olduğunda authentication ve authorization middleware’ini otomatik olarak ekleyebilir. Ancak CORS veya başka middleware’lerle özel sıra gerekiyorsa açık registration gerekir. Reviewer yalnızca “UseAuthorization satırı yok” diyerek finding üretmemeli, kullanılan hosting modelini ve generated pipeline’ı doğrulamalıdır.

4. [AllowAnonymous] beklenenden daha geniş bir alanı açabilir

[AllowAnonymous] bilinçli public endpoint’ler için gereklidir. Tehlike, attribute’un yalnızca en yakın method’u etkilediğinin varsayılmasıdır.

Microsoft’un ASP.NET Core analyzer dokümantasyonunda da belirtildiği gibi, daha uzakta tanımlanmış bir [AllowAnonymous], daha yakın görünen [Authorize] kullanımını override edebilir:

[AllowAnonymous]
public abstract class PublicControllerBase : ControllerBase
{
}

[Authorize]
[Route("api/admin")]
public sealed class AdminController : PublicControllerBase
{
    [HttpGet("audit")]
    public IActionResult GetAudit() => Ok();
}

Kod okuyucusu AdminController üzerindeki [Authorize] nedeniyle endpoint’in protected olduğunu düşünebilir. Base class’tan gelen [AllowAnonymous] bu beklentiyi bozar.

Benzer risk aynı controller içinde de oluşur:

[AllowAnonymous]
[Authorize(Policy = "Administrator")]
[ApiController]
[Route("api/maintenance")]
public sealed class MaintenanceController : ControllerBase
{
    [HttpPost("reindex")]
    public IActionResult Reindex() => Accepted();
}

Burada [AllowAnonymous], authorization check’ini bypass eder. Attribute’ların görsel olarak birbirine yakın olması güvenli bir birleşim üretmez.

AllowAnonymous, authentication’ı tamamen kapatmak değildir

AllowAnonymous, credential gönderilmediğinde endpoint’in anonymous çalışabilmesini sağlar. Geçerli credential gönderilirse authentication middleware identity oluşturabilir. Bu yüzden optional authentication endpoint’lerinde User.Identity.IsAuthenticated farklı request’lerde farklı olabilir.

Bu davranış aşağıdaki gibi karmaşık branching oluşturuyorsa ayrıca incelenmelidir:

[AllowAnonymous]
[HttpGet("catalog")]
public async Task<IActionResult> GetCatalog()
{
    if (User.Identity?.IsAuthenticated == true)
    {
        return Ok(await catalog.GetPersonalizedAsync(User));
    }

    return Ok(await catalog.GetPublicAsync());
}

Bu pattern kendi başına zafiyet değildir. Ancak personalized branch’in claim veya tenant bilgisini güvenilir biçimde alıp almadığı, invalid credential geldiğinde fail-open davranış oluşup oluşmadığı ve public response’a private field karışıp karışmadığı test edilmelidir.

Route group içinde public istisna

Minimal API route group’larında metadata inheritance gözden kaçabilir:

var admin = app.MapGroup("/api/admin")
    .RequireAuthorization("Administrator");

admin.MapGet("/status", GetStatus)
    .AllowAnonymous();

admin.MapPost("/reindex", Reindex);

/status için istisna meşru olabilir. Fakat copy-paste sırasında .AllowAnonymous() yanlış endpoint’e taşınırsa group policy’si o endpoint için devre dışı kalır. Review route registration’ı zincirin sonuna kadar okumalıdır.

İnceleme kontrolü

AllowAnonymous için şu sorular cevaplanmalıdır:

  • Attribute base class veya inherited type üzerinden geliyor mu?
  • Controller, action, PageModel, endpoint ve route group metadata’sında birlikte neler var?
  • Public olma gerekçesi requirement ile doğrulanmış mı?
  • Endpoint hem anonymous hem authenticated çalışıyorsa iki branch de test edilmiş mi?
  • Public endpoint hassas object identifier kabul ediyor mu?
  • Public endpoint service katmanında privileged operation’a ulaşabiliyor mu?
  • .AllowAnonymous() group-level RequireAuthorization() kullanımını etkisizleştiriyor mu?
  • ASP0026 analyzer uyarıları build’de etkin mi, yoksa bastırılmış mı?

5. Role-based authorization hataları kaynak koddan nasıl bulunur?

Role kontrolü basit görünür. Bu yüzden yanlış semantics ve fazla geniş role tasarımı uzun süre fark edilmeyebilir.

ASP.NET Core’da aşağıdaki kullanım OR anlamına gelir:

[Authorize(Roles = "Admin,SuperUser")]
public IActionResult GetSettings() => Ok();

Kullanıcının Admin veya SuperUser role’lerinden birine sahip olması yeterlidir.

İki ayrı attribute ise AND davranışı üretir:

[Authorize(Roles = "Admin")]
[Authorize(Roles = "SuperUser")]
public IActionResult RotateKeys() => Accepted();

Burada kullanıcının her iki role’e de sahip olması gerekir.

Bu ayrım code review’da yalnızca syntax ayrıntısı değildir. Geliştirici “Admin ve SuperUser” yazdığını düşünerek comma-separated tek attribute kullanmış olabilir. Requirement gerçekten iki ayrı role’ün aynı identity’de bulunmasını mı istiyor, yoksa iki farklı actor ile maker-checker mı gerektiriyor, o da ayrıca sorgulanmalıdır.

İki role, iki ayrı kişi anlamına gelmez

Bir kullanıcının hem PaymentCreator hem PaymentApprover role’üne sahip olması mümkünse aşağıdaki endpoint maker-checker kuralını sağlamaz:

[Authorize(Roles = "PaymentApprover")]
[HttpPost("{paymentId:guid}/approve")]
public async Task<IActionResult> Approve(Guid paymentId)
{
    await paymentService.ApproveAsync(paymentId);
    return NoContent();
}

Role yalnızca kullanıcının genel olarak approval yapabileceğini gösterir. Belirli payment’ı kendisinin oluşturup oluşturmadığını göstermez.

Güvenli karar resource ve actor ilişkisini de içerir:

var payment = await db.Payments
    .SingleOrDefaultAsync(x => x.Id == paymentId);

if (payment is null)
{
    return NotFound();
}

var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);

if (payment.CreatedByUserId == actorId)
{
    return Forbid();
}

if (payment.Status != PaymentStatus.PendingApproval)
{
    return Conflict();
}

Bu kontrollerin action içinde dağınık bırakılması yerine domain service veya resource-based authorization ile merkezileştirilmesi daha sürdürülebilir olabilir. Önemli olan aynı invariant’ın bütün caller’larda enforce edilmesidir.

Role claim’inin kaynağı ve mapping’i

User.IsInRole("Admin") çağrısının güvenilirliği, identity içindeki role claim’lerinin nasıl üretildiğine bağlıdır.

Reviewer şu hattı izlemelidir:

  1. 1Token hangi issuer tarafından üretiliyor?
  2. 2Hangi authentication scheme token’ı kabul ediyor?
  3. 3RoleClaimType hangi claim’e map ediliyor?
  4. 4Claim değeri upstream identity provider tarafından mı, uygulama tarafından mı üretiliyor?
  5. 5User-controlled registration veya profile field role claim’e dönüşebiliyor mu?
  6. 6Birden fazla issuer aynı role adını farklı anlamlarda kullanıyor mu?
  7. 7Role değişikliği mevcut token veya long-lived session’da ne zaman etkili oluyor?

Örneğin Admin role’ünün bir partner identity provider’da “partner tenant admin”, uygulamanın kendi identity sisteminde ise “platform admin” anlamına gelmesi ciddi privilege confusion oluşturabilir. Sadece string eşleşmesi business scope’u ayıramaz.

Büyük ve kalıcı role’ler fazla yetki biriktirir

Admin gibi tek bir role’e export, refund, user management, tenant switch ve key rotation yetkilerinin tamamını bağlamak least privilege açısından zayıftır. Role-based authorization bu durumda coarse-grained gate olarak kullanılabilir, ancak hassas operation’lar için daha dar policy ve resource check gerekir.

Kaynak kodda şu pattern’ler incelenmelidir:

  • Aynı Admin kontrolünün çok farklı riskteki operation’larda kullanılması
  • Role string’lerinin controller’larda dağınık olması
  • Eski role adlarının migration sonrasında hâlâ kabul edilmesi
  • Contains("Admin") gibi substring kontrolleri
  • Case normalization’ın identity provider ve application arasında farklı olması
  • User veya tenant tarafından değiştirilebilir field’ın role’e map edilmesi
  • Cache’lenen role’lerin iptal ve değişiklik sonrası uzun süre geçerli kalması
  • Support veya service role’ünün platform-wide data erişimine dönüşmesi

6. Claim ve policy kontrolü varlığı değil, doğru değeri doğrulamalıdır

Claim-based authorization, role’den daha bağlamsal kararlar kurmaya imkân verir. Fakat claim’in yalnızca varlığını kontrol etmek ile izin verilen değerini kontrol etmek farklıdır.

Aşağıdaki policy, permission adlı herhangi bir claim’i olan kullanıcıyı kabul eder:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanExport", policy =>
        policy.RequireClaim("permission"));
});

Kullanıcıda permission=profile.read bulunması bile requirement’ı karşılayabilir. Beklenen değer report.export ise açıkça belirtilmelidir:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanExport", policy =>
        policy.RequireClaim("permission", "report.export"));
});

Policy içindeki requirement’lar AND ile birleşir

Bir policy’ye birden fazla requirement eklendiğinde hepsinin başarılı olması gerekir:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TenantInvoiceExport", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("permission", "invoice.export");
        policy.RequireRole("Finance");
    });
});

Bu policy authenticated user, invoice.export permission’ı ve Finance role’ünü birlikte ister.

Birden fazla policy endpoint’e uygulanmışsa onların da birlikte karşılanması gerekir:

[Authorize(Policy = "FinanceMember")]
[Authorize(Policy = "InvoiceExporter")]
public IActionResult ExportInvoices() => File([], "text/csv");

Burada geliştirici OR bekliyorsa authorization gereğinden dar olur. Tersine tek requirement için birden fazla handler kayıtlıysa handler’lardan biri başarı üreterek OR benzeri model kurabilir. Bu semantics’in bilinmemesi beklenenden geniş erişime de yol açabilir.

RequireAssertion içindeki fail-open mantık

Inline assertion kısa policy’ler için kullanışlıdır. Karmaşıklaştıkça review edilmesi zorlaşır:

options.AddPolicy("CanManageTenant", policy =>
    policy.RequireAssertion(context =>
        context.User.IsInRole("PlatformAdmin") ||
        context.User.HasClaim("tenant_admin", "true")));

Burada tenant_admin=true claim’i yalnızca belirli tenant için anlamlıysa policy eksiktir. Resource veya route’taki tenant ile claim’deki tenant bağlanmamıştır. Kullanıcı bir tenant’ın administrator’ı olduğu için bütün tenant’ları yönetebilir hâle gelebilir.

Claim’i resource ile bağlamak gerekir:

public sealed record TenantAccessRequirement(string Permission)
    : IAuthorizationRequirement;

Ardından handler, current user’ın permission’ını talep edilen Tenant resource’u için değerlendirmelidir.

Policy name ve dynamic policy riskleri

Custom IAuthorizationPolicyProvider, Permission:invoice.read gibi dinamik policy adlarını runtime’da requirement’a dönüştürebilir. İnceleme yalnızca AddPolicy registration’larını ararsa bu policy’leri göremez.

Dynamic policy provider’da şu hatalar aranmalıdır:

  • Tanınmayan policy adında null dönmek yerine permissive policy oluşturulması
  • Policy name parse edilirken prefix veya delimiter hatası
  • Case-insensitive eşleşmenin permission namespace’lerini karıştırması
  • Empty permission’ın authenticated user’a izin vermesi
  • Cache key’in tenant, issuer veya environment bilgisini içermemesi
  • Fallback provider’a yanlış delegation
  • Request’ten gelen string’in doğrudan policy name yapılması

Policy mekanizması authorization’ı otomatik olarak güvenli yapmaz. Yalnızca kararın ifade edildiği framework’ü sağlar.

7. Authorization handler semantics yanlış okunursa policy beklenenden geniş çalışabilir

Custom handler’lar .NET yetkilendirme incelemesinin en kritik noktalarındandır. Kod çoğu zaman kısa görünür, fakat Succeed, hiçbir şey yapmama ve Fail arasında önemli fark vardır.

Başarı, sessiz başarısızlık ve kesin başarısızlık

Bir handler requirement karşılandığında context.Succeed(requirement) çağırır:

public sealed class ActiveEmployeeHandler
    : AuthorizationHandler<ActiveEmployeeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ActiveEmployeeRequirement requirement)
    {
        if (context.User.HasClaim("employment_status", "active"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Koşul karşılanmadığında hiçbir şey yapmamak, ilgili handler’ın başarı vermediği anlamına gelir. Aynı requirement için başka handler varsa o handler başarı verebilir.

context.Fail() ise başka handler başarı verse bile authorization sonucunu kesin olarak başarısız yapar. Bu nedenle Fail yalnızca hard-deny semantics gerçekten isteniyorsa kullanılmalıdır.

Aynı requirement için birden fazla handler OR davranışı kurabilir

Örneğin BuildingEntryRequirement için hem employee badge hem temporary pass handler’ı kayıtlıysa bunlardan birinin Succeed çağırması yeterli olabilir. Bu kasıtlı bir OR modelidir.

Risk, reviewer’ın tek bir handler’ı okuyup kararı tamamlanmış sanmasıdır. DI container’a kayıtlı bütün IAuthorizationHandler implementation’ları bulunmalı ve aynı requirement’ı işleyenler birlikte değerlendirilmelidir.

builder.Services.AddSingleton<IAuthorizationHandler, OwnerHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, PlatformAdminHandler>();

OwnerHandler yalnızca resource owner’a izin veriyor olabilir. PlatformAdminHandler ise bütün platform administrator’larına izin verir. Finding yorumu iki handler’ın birleşik sonucuna göre yapılmalıdır.

Handler execution order güvenlik kontrolü olmamalıdır

Authorization handler’ların registration veya execution sırasına güvenerek “ilk handler tenant’ı doğrular, ikinci handler permission’a bakar” şeklinde tasarım yapılmamalıdır. Bir policy içinde ayrı requirement’lar varsa hepsi geçmelidir. Ortak prerequisite gerekiyorsa her handler kendi kararını güvenli biçimde vermeli veya tek bir requirement altında atomik karar kurulmalıdır.

Şu pattern risklidir:

public sealed class TenantMarkerHandler
    : AuthorizationHandler<ManageDocumentRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ManageDocumentRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "tenant_id"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Bu handler resource’taki tenant ile claim değerini karşılaştırmıyor. Aynı requirement için kayıtlı olduğu için yalnızca herhangi bir tenant_id claim’i authorization’ı başarılı yapabilir.

Exception ve dependency failure davranışı

Handler harici policy service, permission database veya cache kullanıyorsa failure mode açıkça değerlendirilmelidir.

  • Timeout permission olarak mı, deny olarak mı yorumlanıyor?
  • Exception middleware tarafından 500’e mi dönüşüyor, yoksa catch bloğu Succeed mi çağırıyor?
  • Cache miss ile “permission yok” ayrılıyor mu?
  • Stale cache privilege revocation’ı geciktiriyor mu?
  • Tenant context çözülemediğinde global scope’a mı düşülüyor?
  • Development fallback production’da etkin kalmış mı?

Fail-closed davranış her operasyonel sorunu çözmez. Ancak authorization kararı için gerekli veri bulunamadığında erişim vermek çoğu hassas operation için kabul edilemez.

8. Resource-based authorization olmadan object-level kontrol eksik kalır

Attribute-based authorization, endpoint çalıştırılmadan önce değerlendirilir. O aşamada çoğu zaman talep edilen domain object henüz database’den yüklenmemiştir. Bu nedenle “kullanıcı bu document’ın owner’ı mı?” gibi bir karar yalnızca [Authorize] attribute’u ile verilemez.

ASP.NET Core bu durum için IAuthorizationService ile imperative, resource-based authorization sunar.

Zafiyetli örnek

[Authorize(Policy = "InvoiceReader")]
[HttpGet("{invoiceId:guid}")]
public async Task<IActionResult> Get(Guid invoiceId)
{
    var invoice = await db.Invoices.FindAsync(invoiceId);

    if (invoice is null)
    {
        return NotFound();
    }

    return Ok(invoice);
}

InvoiceReader kullanıcının genel olarak fatura okuyabildiğini gösterebilir. Belirli faturaya erişimini göstermiyorsa horizontal privilege escalation mümkündür.

Operation tanımları

CRUD ve domain action’ları için OperationAuthorizationRequirement kullanılabilir:

using Microsoft.AspNetCore.Authorization.Infrastructure;

public static class InvoiceOperations
{
    public static readonly OperationAuthorizationRequirement Read =
        new() { Name = nameof(Read) };

    public static readonly OperationAuthorizationRequirement Update =
        new() { Name = nameof(Update) };

    public static readonly OperationAuthorizationRequirement Refund =
        new() { Name = nameof(Refund) };
}

Resource handler

public sealed class InvoiceAuthorizationHandler
    : AuthorizationHandler<OperationAuthorizationRequirement, Invoice>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement,
        Invoice invoice)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        var tenantId = context.User.FindFirstValue("tenant_id");

        if (string.IsNullOrWhiteSpace(userId) ||
            string.IsNullOrWhiteSpace(tenantId))
        {
            return Task.CompletedTask;
        }

        if (invoice.TenantId != tenantId)
        {
            return Task.CompletedTask;
        }

        if (requirement.Name == InvoiceOperations.Read.Name &&
            (invoice.CustomerUserId == userId ||
             context.User.IsInRole("Finance")))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == InvoiceOperations.Refund.Name &&
            context.User.HasClaim("permission", "invoice.refund") &&
            invoice.Status == InvoiceStatus.Paid)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Registration:

builder.Services.AddScoped<IAuthorizationHandler, InvoiceAuthorizationHandler>();

Action:

[Authorize]
[HttpGet("{invoiceId:guid}")]
public async Task<IActionResult> Get(Guid invoiceId)
{
    var invoice = await db.Invoices
        .AsNoTracking()
        .SingleOrDefaultAsync(x => x.Id == invoiceId);

    if (invoice is null)
    {
        return NotFound();
    }

    var result = await authorizationService.AuthorizeAsync(
        User,
        invoice,
        InvoiceOperations.Read);

    if (!result.Succeeded)
    {
        return Forbid();
    }

    return Ok(mapper.Map<InvoiceDto>(invoice));
}

Bu örnek modelin başlangıcıdır. Gerçek projede platform administrator istisnası, delegated access, privacy restriction, account state ve field-level filtering gibi ek kurallar olabilir.

Resource yüklemeden önce tenant-bound query

Resource-based authorization kullanmak, database’den herhangi bir tenant’ın kaydını önce yüklemenin her zaman doğru olduğu anlamına gelmez. Multi-tenant sistemde defense in depth için query de tenant context’e bağlanabilir:

var tenantId = tenantContext.RequiredTenantId;

var invoice = await db.Invoices
    .AsNoTracking()
    .SingleOrDefaultAsync(x =>
        x.Id == invoiceId &&
        x.TenantId == tenantId);

Ardından operation-specific authorization yine uygulanabilir. Query filter data isolation sağlar, authorization handler actor, operation ve state ilişkisini değerlendirir.

404 mü 403 mü?

Başka tenant’a ait resource’un varlığını açıklamamak için bazı API’ler unauthorized object access durumunda 404 döndürür. Bu karar enumeration riskini azaltabilir. Ancak internal logging ve audit tarafında “bulunamadı” ile “bulundu fakat yetki reddedildi” ayrılmalıdır.

Response code seçimi authorization kontrolünün yerine geçmez. NotFound() dönmek için önce object-level karar verilmelidir. Sadece ID’yi gizlemek zafiyeti kapatmaz.

9. Multi-tenant authorization’da tenantId request’ten alınmamalıdır

Multi-tenant uygulamalarda en kritik invariant şudur:

Not

Kullanıcının işlem yaptığı tenant, user-controlled request alanından değil server-side doğrulanmış tenant context’ten türetilmelidir.

Zafiyetli controller:

[Authorize]
[HttpGet]
public async Task<IActionResult> List([FromQuery] string tenantId)
{
    var invoices = await db.Invoices
        .Where(x => x.TenantId == tenantId)
        .ToListAsync();

    return Ok(invoices);
}

Kullanıcı authenticated olsa bile query string’i değiştirerek başka tenant’ın verisini isteyebilir.

Route’tan gelen tenant ID de kendiliğinden güvenilir değildir:

[Authorize]
[HttpGet("/api/tenants/{tenantId}/invoices")]
public Task<List<Invoice>> List(string tenantId)
{
    return db.Invoices
        .Where(x => x.TenantId == tenantId)
        .ToListAsync();
}

Route yalnızca talebi tanımlar. Kullanıcının o tenant’a üye olduğunu kanıtlamaz.

Doğrulanmış tenant context

Tenant context authentication sonucu, membership database’i ve gerektiğinde delegated access kaydı üzerinden oluşturulabilir:

public interface ITenantContext
{
    string RequiredTenantId { get; }
    string RequiredUserId { get; }
}

Controller bu context’i kullanır:

[Authorize(Policy = "TenantMember")]
[HttpGet]
public async Task<IActionResult> List()
{
    var invoices = await db.Invoices
        .AsNoTracking()
        .Where(x => x.TenantId == tenantContext.RequiredTenantId)
        .ToListAsync();

    return Ok(invoices.Select(mapper.Map<InvoiceSummaryDto>));
}

Kullanıcının tenant seçmesine izin verilen ürünlerde bile request’teki seçim doğrudan authority değildir. Server, seçilen tenant için membership ve permission doğrulayıp context üretmelidir.

Platform administrator bypass’ı görünür olmalıdır

Şu pattern tehlikelidir:

var query = db.Invoices.AsQueryable();

if (!User.IsInRole("Admin"))
{
    query = query.Where(x => x.TenantId == tenantContext.RequiredTenantId);
}

Admin role’ünün tenant administrator mı, platform administrator mı olduğu belirsizdir. Yanlış role mapping bütün tenant filter’ını kaldırabilir.

Daha açık bir policy kullanılmalıdır:

if (!await platformAccess.CanReadAcrossTenantsAsync(User))
{
    query = query.Where(x => x.TenantId == tenantContext.RequiredTenantId);
}

Cross-tenant erişim audit edilmeli, dar operation’larla sınırlandırılmalı ve export gibi yüksek etkili function’larda ek kontrol uygulanmalıdır.

10. EF Core global query filter faydalıdır, tek başına authorization değildir

EF Core global query filter, bir entity türü sorgulandığında otomatik Where koşulu ekler. Soft delete ve multi-tenancy sık kullanım alanlarıdır.

Basit tenant filter:

public sealed class AppDbContext(
    DbContextOptions<AppDbContext> options,
    ITenantContext tenantContext)
    : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Invoice>()
            .HasQueryFilter(x =>
                x.TenantId == tenantContext.RequiredTenantId);
    }
}

Bu kontrol, geliştiricinin her query’de tenant koşulunu tekrar yazma zorunluluğunu azaltır. Ancak şu nedenlerle tek başına authorization sistemi olarak görülmemelidir:

  • Filter actor’ın operation yetkisini değerlendirmez
  • IgnoreQueryFilters() ile devre dışı bırakılabilir
  • Raw SQL veya farklı data-access path’i filter’ı atlayabilir
  • Başka DbContext implementation’ında tanımlı olmayabilir
  • Entity relation ve projection davranışları yanlış veri görünürlüğü üretebilir
  • Background job’da tenant context yanlış veya boş olabilir
  • Platform administrator için yazılan bypass bütün caller’lara sızabilir
  • Bulk update veya delete path’leri ayrıca incelenmelidir

IgnoreQueryFilters() yüksek sinyalli bir review noktasıdır

var invoices = await db.Invoices
    .IgnoreQueryFilters()
    .Where(x => x.Status == InvoiceStatus.Overdue)
    .ToListAsync();

Bu kod migration, reconciliation veya platform reporting için meşru olabilir. Fakat tenant filter ile soft-delete filter’ını birlikte kaldırabilir. Çağıran kullanıcının cross-tenant operation yetkisi ve response’un dışarı sızıp sızmadığı doğrulanmalıdır.

Review sırasında her IgnoreQueryFilters() call site’i için şu bilgiler çıkarılmalıdır:

  • Hangi named veya combined filter’lar devre dışı kalıyor?
  • Call site hangi entry point’lerden çağrılabiliyor?
  • Cross-tenant veri yalnızca aggregate mı, row-level mı dönüyor?
  • Platform-wide permission açıkça doğrulanıyor mu?
  • Query sonucu cache’e veya export’a yazılıyor mu?
  • Soft-deleted hassas kayıtlar da görünür oluyor mu?
  • Audit event üretiliyor mu?

EF Core 10 named query filter

EF Core 10, aynı entity üzerinde birden fazla filter’ı adlandırmaya imkân verir:

modelBuilder.Entity<Invoice>()
    .HasQueryFilter(
        "SoftDeletionFilter",
        x => !x.IsDeleted)
    .HasQueryFilter(
        "TenantFilter",
        x => x.TenantId == tenantContext.RequiredTenantId);

Bu özellik filter’ların seçici biçimde yönetilmesini sağlar. Örneğin soft-deleted kayıtları incelemek için yalnızca soft-delete filter’ı kapatılırken tenant filter korunabilir.

var invoices = await db.Invoices
    .IgnoreQueryFilters(["SoftDeletionFilter"])
    .ToListAsync();

Kodun target ettiği EF Core version doğrulanmalıdır. Daha eski version’larda filter’lar tek expression içinde && ile birleştirilir. Version’a göre çalışmayan bir güvenlik varsayımı yapılmamalıdır.

Filter overwrite hatası

EF Core 10 öncesinde aynı entity için HasQueryFilter iki kez çağrılırsa ikinci çağrı ilk filter’ı overwrite eder:

modelBuilder.Entity<Invoice>()
    .HasQueryFilter(x => !x.IsDeleted);

modelBuilder.Entity<Invoice>()
    .HasQueryFilter(x =>
        x.TenantId == tenantContext.RequiredTenantId);

Bu örnekte soft-delete filter’ı korunmaz. Ters sıra kullanılırsa daha tehlikeli biçimde tenant filter kaybolabilir.

Eski version’larda combined expression gerekir:

modelBuilder.Entity<Invoice>()
    .HasQueryFilter(x =>
        !x.IsDeleted &&
        x.TenantId == tenantContext.RequiredTenantId);

Context lifetime ve tenant capture

DbContext normalde scoped kullanılır. Tenant bilgisinin singleton service içinde mutable tutulması veya farklı request’ler arasında paylaşılması cross-tenant data leak oluşturabilir. Özellikle pooled context, background processing ve factory kullanımlarında tenant değerinin doğru instance’a nasıl taşındığı incelenmelidir.

IDbContextFactory<TContext> ile context oluşturan worker, HTTP request’e bağlı ITenantContext bulamayabilir. “Tenant yoksa filter uygulama” fallback’i varsa background caller’ın bütün tenant’ları görmesine yol açabilir:

modelBuilder.Entity<Invoice>()
    .HasQueryFilter(x =>
        tenantContext.TenantId == null ||
        x.TenantId == tenantContext.TenantId);

Bu ifade fail-open’dır. Tenant context çözülemediğinde filter herkese izin verir. Global operation gerçekten gerekiyorsa ayrı, açık ve dar bir data-access path tasarlanmalıdır.

11. Minimal API ve route group incelemesinde metadata zinciri izlenmelidir

Minimal API kısa kodla çok sayıda endpoint üretir. Authorization’ın route registration zincirinde görünmesi avantajdır. Aynı kısalık, group inheritance ve extension method’lar nedeniyle review’ı zorlaştırabilir.

Güvenli group baseline

var invoices = app.MapGroup("/api/invoices")
    .RequireAuthorization("InvoiceUser");

invoices.MapGet("/", ListInvoices);
invoices.MapGet("/{invoiceId:guid}", GetInvoice);
invoices.MapPost("/{invoiceId:guid}/refund", RefundInvoice)
    .RequireAuthorization("InvoiceRefund");

Group policy bütün child endpoint’lere uygulanır. Refund endpoint’i ek policy alır. Policy’lerin birlikte değerlendirildiği unutulmamalıdır.

Aynı handler farklı registration ile açılabilir

app.MapGet("/api/invoices/{id:guid}", GetInvoice)
    .RequireAuthorization("InvoiceUser");

app.MapGet("/internal/debug/invoices/{id:guid}", GetInvoice);

GetInvoice function’ı ilk path’te protected, ikinci path’te metadata’sızdır. Handler’ın içinde object-level authorization yoksa ikinci registration bypass olur.

Bu nedenle review function’dan route’a ve route’tan function’a iki yönlü yapılmalıdır:

  1. 1Hassas handler’ın bütün registration noktalarını bulun.
  2. 2Her route’un metadata’sını çıkarın.
  3. 3Handler’ın endpoint dışından çağrılıp çağrılmadığını kontrol edin.
  4. 4Ortak security invariant’ın yalnızca registration’a mı, domain katmanına mı bağlı olduğunu belirleyin.

Extension method içinde kaybolan policy

public static RouteGroupBuilder MapBillingApi(
    this IEndpointRouteBuilder endpoints)
{
    var group = endpoints.MapGroup("/api/billing");

    group.MapGet("/invoices", ListInvoices);
    group.MapPost("/refunds", CreateRefund);

    return group;
}

Program.cs içinde aşağıdaki kullanım güvenli görünebilir:

app.MapBillingApi()
    .RequireAuthorization("BillingUser");

Reviewer extension method’un return ettiği builder’a sonradan eklenen metadata’nın hangi child endpoint’lere uygulandığını framework version’ı ve registration zamanı açısından doğrulamalıdır. Daha okunabilir yaklaşım, korumayı group oluşturulurken açıkça uygulamak ve integration test ile endpoint metadata’sını doğrulamaktır.

Filter authorization yerine geçmemelidir

Minimal API endpoint filter içinde custom permission kontrolü yapılabilir. Fakat filter yalnızca eklendiği registration’larda çalışır. Hassas handler başka route’a bağlandığında kontrol kaybolabilir.

invoices.MapPost("/{id:guid}/refund", RefundInvoice)
    .AddEndpointFilter<RefundAuthorizationFilter>();

Bu filter’ın:

  • DI lifetime’ı
  • Failure response’u
  • Route value parsing’i
  • Tenant context kaynağı
  • Resource loading davranışı
  • Exception halinde erişim verip vermediği
  • Bütün refund registration’larına eklenip eklenmediği

incelenmelidir.

Framework’ün RequireAuthorization ve IAuthorizationService mekanizması varken custom filter ile ikinci bir authorization framework’ü kurmak kararların dağılmasına yol açabilir.

12. MVC ve Razor Pages’te görünmeyen yetki boşlukları

Controller-based projelerde attribute inheritance, action override, conventional routing ve filter order birlikte değerlendirilmelidir.

Controller policy, object policy değildir

[Authorize(Roles = "Support")]
[ApiController]
[Route("api/customers")]
public sealed class CustomersController : ControllerBase
{
    [HttpGet("{customerId:guid}/identity-document")]
    public Task<IActionResult> DownloadIdentityDocument(Guid customerId)
    {
        // ...
    }
}

Support role’ü bütün müşteri belgelerini görebilir mi, yalnızca kendisine atanmış case’leri mi görebilir, görüntüleme için ayrı reason code gerekir mi? Controller-level role bu soruların hiçbirini cevaplamaz.

Action override ve inherited metadata

Base controller ortak [Authorize] taşıyabilir. Derived controller action’ı override ederken [AllowAnonymous] alabilir veya başka base type’tan beklenmeyen metadata gelebilir. Runtime endpoint metadata’sı yalnızca görünen source file’dan çıkarılmamalıdır.

Build sonrası endpoint inspection testi yararlıdır:

[Fact]
public void SensitiveEndpoints_MustRequireAuthorization()
{
    var endpoints = app.Services
        .GetRequiredService<EndpointDataSource>()
        .Endpoints;

    var sensitive = endpoints.Where(e =>
        e.DisplayName?.Contains("Admin", StringComparison.OrdinalIgnoreCase)
        == true);

    foreach (var endpoint in sensitive)
    {
        Assert.NotEmpty(endpoint.Metadata
            .GetOrderedMetadata<IAuthorizeData>());

        Assert.Null(endpoint.Metadata.GetMetadata<IAllowAnonymous>());
    }
}

Bu test naming convention’a bağlıysa tek başına yeterli değildir. Route manifest veya explicit endpoint classification ile güçlendirilmelidir.

Razor Pages authorization convention

Razor Pages’te page ve folder bazlı convention kullanılabilir:

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/Admin", "Administrator");
    options.Conventions.AllowAnonymousToPage("/Admin/Login");
});

Buradaki önemli davranış şudur: anonymous işaretlenmiş bir folder içindeki tek bir page’i tekrar authorize etmek beklenen sonucu üretmez. AllowAnonymous precedence nedeniyle daha dar sayfayı yeniden kapatma yaklaşımı çalışmaz.

Riskli configuration:

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AllowAnonymousToFolder("/Public");
    options.Conventions.AuthorizePage(
        "/Public/InternalReport",
        "Employee");
});

InternalReport üzerinde hem anonymous hem authorize filter bulunur ve anonymous davranış üstün gelir. Folder’ı public yapmak yerine public page’leri explicit tanımlamak gerekir.

Razor Pages handler-level beklenti

Razor Pages authorization page seviyesinde değerlendirilir. Bir PageModel içindeki OnGet, OnPostDelete ve OnPostApprove handler’larına farklı [Authorize] attribute’ları koyarak MVC action benzeri handler-level authorization beklenmemelidir.

Tek page hem görüntüleme hem hassas mutation içeriyorsa mutation içinde imperative authorization uygulanmalı veya ayrı PageModel/endpoint’e bölünmelidir:

[Authorize(Policy = "InvoiceUser")]
public sealed class DetailsModel : PageModel
{
    public async Task<IActionResult> OnPostRefundAsync(Guid id)
    {
        var invoice = await db.Invoices.FindAsync(id);

        if (invoice is null)
        {
            return NotFound();
        }

        var result = await authorizationService.AuthorizeAsync(
            User,
            invoice,
            InvoiceOperations.Refund);

        if (!result.Succeeded)
        {
            return Forbid();
        }

        await refundService.CreateAsync(invoice);
        return RedirectToPage("./Details", new { id });
    }
}

13. Service katmanı ve HTTP dışı caller’lar authorization’ı bypass edebilir

Controller policy’si yalnızca HTTP endpoint’e girişte çalışır. Domain veya application service başka bir yerden çağrıldığında aynı attribute otomatik olarak uygulanmaz.

Örnek service:

public sealed class RefundService(AppDbContext db)
{
    public async Task CreateAsync(
        Guid invoiceId,
        decimal amount,
        CancellationToken cancellationToken = default)
    {
        var invoice = await db.Invoices.FindAsync(
            [invoiceId],
            cancellationToken);

        // Refund oluşturuluyor
    }
}

Controller bunu güçlü policy ile çağırabilir:

[Authorize(Policy = "InvoiceRefund")]
[HttpPost("{invoiceId:guid}/refund")]
public Task<IActionResult> Refund(Guid invoiceId, RefundRequest request)
{
    // ...
}

Fakat aynı service aşağıdaki yerlerden de çağrılabilir:

  • Payment provider webhook
  • Queue consumer
  • Scheduled retry job
  • Administrator CLI
  • Internal gRPC service
  • Integration test fixture
  • Başka controller veya Minimal API handler

Authorization yalnızca controller’da ise yeni caller security check’i atlar.

Caller identity ile business authority ayrılmalıdır

Background worker’ın ClaimsPrincipal taşımaması normal olabilir. Bu, worker’ın sınırsız yetkili olması gerektiği anlamına gelmez. System action’ları için açık bir execution context ve capability modeli kurulabilir:

public sealed record RefundCommand(
    Guid InvoiceId,
    decimal Amount,
    RefundActor Actor,
    string Reason,
    string IdempotencyKey);

public sealed record RefundActor(
    RefundActorType Type,
    string SubjectId,
    string TenantId);

Domain service şunları doğrulayabilir:

  • Actor type bu operation için izinli mi?
  • Actor’ın tenant’ı invoice tenant’ıyla aynı mı?
  • User action ise permission kontrolü yapılmış mı?
  • Provider action ise signed event ile invoice ilişkisi doğrulanmış mı?
  • Refund amount kalan refundable amount’ı aşıyor mu?
  • Invoice doğru state’te mi?
  • Aynı idempotency key daha önce işlendi mi?

Bu yaklaşım HTTP authorization ile domain authorization’ı karıştırmaz. Her caller’ın authority’sini açık kılar.

Method parametresi olarak isAdmin

Aşağıdaki pattern özellikle risklidir:

public Task DeleteUserAsync(Guid userId, bool isAdmin)
{
    if (!isAdmin)
    {
        throw new ForbiddenException();
    }

    // ...
}

isAdmin değerini caller belirler. Yeni bir controller, message consumer veya test helper yanlışlıkla true geçebilir. Service güvenilir identity veya authorization abstraction üzerinden kendi kararını üretmeli, caller’dan güvenlik sonucu kabul etmemelidir.

Benzer riskli parametreler:

  • skipAuthorization
  • bypassTenantFilter
  • runAsSystem
  • includeAllTenants
  • canApprove
  • ignoreOwnership
  • isInternal

Bu flag’ler tamamen yasak değildir. Migration ve system operation’da ihtiyaç olabilir. Ancak capability type, dar interface, ayrı service ve audit ile açıkça sınırlandırılmalıdır. Public application service method’una boolean bypass eklemek güvenlik modelini kırılgan yapar.

14. SignalR ve gRPC ayrı authorization surface’leridir

HTTP controller’lar güçlü biçimde korunurken SignalR hub veya gRPC method’ları anonymous kalabilir.

SignalR varsayılanı

ASP.NET Core SignalR’da hub method’ları varsayılan olarak unauthenticated kullanıcılar tarafından çağrılabilir. Authentication gerekiyorsa hub veya method açıkça korunmalıdır:

[Authorize]
public sealed class OperationsHub : Hub
{
    public Task Subscribe(string tenantId)
    {
        // ...
        return Task.CompletedTask;
    }

    [Authorize(Policy = "IncidentCommander")]
    public Task CloseIncident(Guid incidentId)
    {
        // ...
        return Task.CompletedTask;
    }
}

Hub üzerinde [Authorize] authentication sağlar. Subscribe(string tenantId) için tenant membership’i yine ayrıca doğrulanmalıdır. Client’ın group adına katılabilmesi, o group’a ait event’leri görme yetkisi anlamına gelmemelidir.

Zafiyetli pattern:

public Task JoinTenant(string tenantId)
{
    return Groups.AddToGroupAsync(
        Context.ConnectionId,
        $"tenant:{tenantId}");
}

Güvenli model:

public async Task JoinTenant(string tenantId)
{
    var tenant = await tenantRepository.FindAsync(tenantId);

    if (tenant is null)
    {
        throw new HubException("Resource not found");
    }

    var result = await authorizationService.AuthorizeAsync(
        Context.User!,
        tenant,
        "TenantEventSubscriber");

    if (!result.Succeeded)
    {
        throw new HubException("Forbidden");
    }

    await Groups.AddToGroupAsync(
        Context.ConnectionId,
        $"tenant:{tenant.Id}");
}

SignalR connection yaşam süresi

SignalR authenticated principal’ı connection kurulduğunda alır ve connection yaşam süresince cache’ler. Framework aktif connection sırasında role veya claim değişikliğini otomatik olarak tekrar doğrulamaz. Token expire olsa bile kullanılan transport ve configuration’a göre connection çalışmaya devam edebilir.

Bu nedenle yüksek etkili hub method’larında:

  • CloseOnAuthenticationExpiration ihtiyacı
  • Privilege revocation sonrası connection kapatma
  • Current permission’ın data store’dan yeniden doğrulanması
  • Group membership’in tenant değişikliği sonrası temizlenmesi
  • Unique user identifier claim’inin gerçekten unique olması

değerlendirilmelidir.

gRPC service ve method authorization

ASP.NET Core gRPC service method’ları da varsayılan olarak anonymous çağrıya açık olabilir. Service veya method seviyesinde [Authorize] uygulanabilir:

[Authorize]
public sealed class BillingGrpcService
    : Billing.BillingBase
{
    [Authorize(Policy = "InvoiceRefund")]
    public override async Task<RefundReply> Refund(
        RefundRequest request,
        ServerCallContext context)
    {
        var user = context.GetHttpContext().User;
        // Resource-based authorization burada uygulanır
        return new RefundReply();
    }
}

Class ve method policy’lerinin ikisi de değerlendirilir. Buna karşılık yalnızca gRPC endpoint registration’da RequireAuthorization kullanılmışsa aynı service’in başka host veya test server’da farklı registration’ı ayrıca kontrol edilmelidir.

15. Field-level authorization ve over-posting hataları

Bir kullanıcının object’i update edebilmesi, object üzerindeki bütün field’ları değiştirebilmesi anlamına gelmez.

Zafiyetli DTO:

public sealed record UpdateUserRequest(
    string DisplayName,
    string Email,
    string Role,
    string TenantId,
    bool IsLocked);

Zafiyetli mapping:

[Authorize]
[HttpPut("me")]
public async Task<IActionResult> UpdateMe(UpdateUserRequest request)
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var user = await db.Users.SingleAsync(x => x.Id == userId);

    mapper.Map(request, user);
    await db.SaveChangesAsync();

    return NoContent();
}

Kullanıcı yalnızca kendi object’ini update ediyor olabilir. Buna rağmen Role, TenantId veya IsLocked gibi server-controlled field’ları değiştirebilir. Bu horizontal IDOR değil, property-level authorization ve mass assignment problemidir.

Capability-specific request model

Self-service endpoint dar DTO kabul etmelidir:

public sealed record UpdateMyProfileRequest(
    string DisplayName,
    string PreferredLanguage);

Mapping explicit yapılabilir:

user.DisplayName = request.DisplayName;
user.PreferredLanguage = request.PreferredLanguage;

Administrator operation’ı ayrı policy ve ayrı DTO ile tanımlanmalıdır:

public sealed record ChangeUserRoleRequest(string Role);

[Authorize(Policy = "UserRoleManager")]
[HttpPut("{userId:guid}/role")]
public async Task<IActionResult> ChangeRole(
    Guid userId,
    ChangeUserRoleRequest request)
{
    // Target user ve role için resource/context kontrolü
    return NoContent();
}

Bu ayrım API contract’ında hangi actor’ın hangi field’ı değiştirebildiğini görünür kılar.

JSON Patch ve dynamic update

JsonPatchDocument<T>, reflection tabanlı update, dictionary binding ve generic repository UpdateFieldsAsync gibi function’lar field-level authorization riskini artırır:

[HttpPatch("{id:guid}")]
public async Task<IActionResult> Patch(
    Guid id,
    JsonPatchDocument<User> patch)
{
    var user = await db.Users.FindAsync(id);
    patch.ApplyTo(user!);
    await db.SaveChangesAsync();
    return NoContent();
}

Reviewer şunları aramalıdır:

  • Patch path allowlist’i var mı?
  • Nested object üzerinden protected field’a ulaşılabiliyor mu?
  • Role veya tenant gibi alanlar bind ediliyor mu?
  • Validation patch öncesi mi, sonrası mı çalışıyor?
  • Authorization hangi operation ve field set’i için veriliyor?
  • Audit log eski ve yeni değeri kaydediyor mu?
  • Bulk update aynı field restriction’ı uyguluyor mu?

Response field authorization

Yetkilendirme yalnızca write tarafında değildir. User object’ini görme izni, password hash, MFA secret, recovery code, internal risk score, salary, national identifier veya başka tenant metadata’sını görme izni anlamına gelmez.

Entity’yi doğrudan response yapmak güvenli değildir:

return Ok(user);

Use-case-specific projection tercih edilmelidir:

var result = await db.Users
    .Where(x =>
        x.Id == userId &&
        x.TenantId == tenantContext.RequiredTenantId)
    .Select(x => new UserSummaryDto(
        x.Id,
        x.DisplayName,
        x.Status))
    .SingleOrDefaultAsync();

Projection yalnızca accidental exposure riskini azaltır. Query’nin actor ve tenant ile bağlanması yine gerekir.

16. SAST neyi bulabilir, uzman reviewer neyi tamamlamalıdır?

SAST .NET yetkilendirme açıklarının bir bölümünde güçlü sinyal üretebilir:

  • [AllowAnonymous] ile [Authorize] çakışması
  • UseAuthorization middleware order hatası
  • Hassas route üzerinde authorization metadata eksikliği
  • Request’ten gelen userId veya tenantId ile doğrudan query
  • FindAsync(id) sonrası ownership check bulunmaması
  • IgnoreQueryFilters() kullanımı
  • User.IsInRole ile dağınık hardcoded role kontrolü
  • HasClaim(type) kullanıp value kontrol etmeme
  • Entity’yi doğrudan response dönme
  • Security-sensitive property içeren broad update DTO
  • skipAuthorization ve benzeri bypass flag’leri

Ancak tool şu soruların cevabını application context olmadan güvenilir biçimde veremez:

  • Bu role gerçekten bu operation’ı yapmalı mı?
  • Tenant administrator cross-tenant rapor alabilir mi?
  • Support kullanıcısı hangi case’lere atanmış?
  • Kullanıcı kendi oluşturduğu işlemi onaylayabilir mi?
  • Invoice bu state’te refund edilebilir mi?
  • PlatformAdminHandler kasıtlı mı, gereğinden geniş mi?
  • Background consumer hangi authority ile çalışıyor?
  • Optional authenticated public endpoint hangi field’ları döndürmeli?
  • İki ayrı güvenli policy’nin birleşimi business rule’u karşılıyor mu?

Otomatik SAST ile uzman manuel incelemenin farklı kanıt türlerini Secure Code Review ile otomatik SAST taraması arasındaki fark yazısında ayrıntılı olarak inceleyebilirsiniz.

Custom rule için yüksek sinyalli pattern

Örneğin aşağıdaki Semgrep benzeri kural taslağı, request parametresinden gelen tenantId ile query kurulmasını aday olarak işaretleyebilir:

rules:
  - id: dotnet-request-tenant-direct-query
    message: Request kaynaklı tenantId doğrudan data query içinde kullanılıyor
    severity: WARNING
    languages:
      - csharp
    patterns:
      - pattern-inside: |
          $RET $METHOD(..., string $TENANT, ...) {
            ...
          }
      - metavariable-regex:
          metavariable: $TENANT
          regex: (?i).*tenant.*id.*
      - pattern: $DB.$SET.Where($X => $X.TenantId == $TENANT)

Bu kural doğrudan finding üretmemelidir. Reviewer şu doğrulamaları yapmalıdır:

  • Parametre request’ten mi, trusted internal caller’dan mı geliyor?
  • Öncesinde membership check var mı?
  • Query filter zaten uygulanıyor mu?
  • Endpoint platform-level operation mı?
  • Sonuç row-level data mı, güvenli aggregate mı?
  • Başka overload veya repository abstraction kontrolü enforce ediyor mu?

Framework analyzer’ları build gate’e alınmalıdır

ASP.NET Core analyzer kuralları özellikle metadata precedence ve middleware order gibi framework-specific hatalarda değerlidir. Ancak warning’in görünmesi yetmez:

  • Analyzer package ve SDK version’ı bütün solution’da tutarlı mı?
  • .editorconfig rule severity’si kapatılmış mı?
  • NoWarn listesinde security-relevant rule var mı?
  • Generated code exclusion’ı gerçek endpoint registration’larını dışarıda bırakıyor mu?
  • CI build warning’leri error’a yükseltiyor mu?
  • Suppression için reason ve review owner var mı?

SAST’ın coverage ve limitation’larını daha geniş çerçevede SAST nedir, hangi açıkları bulur ve hangilerini kaçırır? içeriğinde ele alıyoruz.

17. Authorization testleri role değil actor-resource-operation matrisi üzerinden yazılmalıdır

“Admin 200 alır, anonymous 401 alır” testi authorization modelinin küçük bir parçasını doğrular. Horizontal privilege escalation, tenant isolation ve context-dependent rule’lar için negatif testler gerekir.

Authorization matrix

Kritik operation için en az şu matrix hazırlanabilir:

ActorResource ilişkisiOperationBeklenen sonuç
AnonymousUygulanamazInvoice read401
Customer AKendi invoice’uReadAllow
Customer ACustomer B invoice’uReadDeny
Tenant A financeTenant A invoice’uRefundAllow
Tenant A financeTenant B invoice’uRefundDeny
Invoice creatorKendi oluşturduğu invoiceApproveDeny
Tenant adminAynı tenant user’ıDisableAllow
Tenant adminBaşka tenant user’ıDisableDeny
Platform supportAtanmamış customer case’iRead PIIDeny
Platform auditorCross-tenant aggregateReadAllow

Bu matrix role listesi değildir. Actor’ın resource ile ilişkisini ve operation context’ini de içerir.

Handler unit testi

Resource handler doğrudan test edilebilir:

public sealed class InvoiceAuthorizationHandlerTests
{
    [Fact]
    public async Task Read_Denies_User_From_Different_Tenant()
    {
        var user = new ClaimsPrincipal(
            new ClaimsIdentity(
                [
                    new Claim(
                        ClaimTypes.NameIdentifier,
                        "user-a"),
                    new Claim("tenant_id", "tenant-a"),
                    new Claim(
                        ClaimTypes.Role,
                        "Finance")
                ],
                authenticationType: "Test"));

        var invoice = new Invoice
        {
            Id = Guid.NewGuid(),
            TenantId = "tenant-b",
            CustomerUserId = "user-b"
        };

        var requirement = InvoiceOperations.Read;
        var context = new AuthorizationHandlerContext(
            [requirement],
            user,
            invoice);

        var handler = new InvoiceAuthorizationHandler();
        await handler.HandleAsync(context);

        Assert.False(context.HasSucceeded);
    }
}

Pozitif test kadar negatif ve conflict testleri de yazılmalıdır:

  • Doğru role, yanlış tenant
  • Doğru tenant, yanlış owner
  • Doğru owner, yanlış operation
  • Doğru permission, yanlış resource state
  • Missing claim
  • Duplicate claim ve beklenmeyen value
  • Yanlış issuer’dan gelen aynı claim
  • Disabled veya deleted actor
  • Delegation süresi dolmuş actor
  • Platform role’ü olmayan cross-tenant actor

Endpoint integration testi

Authorization metadata ve gerçek pipeline integration test ile doğrulanmalıdır:

[Theory]
[InlineData("customer-a", "invoice-a", HttpStatusCode.OK)]
[InlineData("customer-a", "invoice-b", HttpStatusCode.NotFound)]
public async Task Invoice_Read_Enforces_Object_Access(
    string actor,
    string invoice,
    HttpStatusCode expected)
{
    using var client = factory.CreateAuthenticatedClient(actor);

    var response = await client.GetAsync(
        $"/api/invoices/{invoice}");

    Assert.Equal(expected, response.StatusCode);
}

Test fixture içindeki authentication handler production claim mapping’ini gereğinden fazla basitleştirmemelidir. Test token’ı bütün claim’leri otomatik ekliyorsa eksik claim senaryoları görünmez kalabilir.

Identifier değişimi tek negatif test değildir

ID değiştirerek başka object’e erişim test edilmelidir. Buna ek olarak:

  • tenantId route, query ve body’de değiştirilir
  • Bulk endpoint’e başka tenant ID’leri içeren liste gönderilir
  • Child object ID’si başka parent altında kullanılır
  • Export filter’ı genişletilir
  • Nested route parent ile child ilişkisinin çeliştiği request gönderilir
  • Update DTO’ya protected field eklenir
  • Aynı service farklı entry point’ten çağrılır
  • SignalR group name ve gRPC resource ID değiştirilir
  • Soft-deleted ve archived resource denenir
  • Platform bypass role’üne benzer isimli role kullanılır

TOCTOU ve state değişikliği

Authorization sonucu ile mutation arasında resource state değişebilir:

var invoice = await db.Invoices.FindAsync(invoiceId);

var result = await authorizationService.AuthorizeAsync(
    User,
    invoice!,
    InvoiceOperations.Refund);

if (!result.Succeeded)
{
    return Forbid();
}

await paymentProvider.RefundAsync(invoice!.PaymentId, amount);
invoice.Status = InvoiceStatus.Refunded;
await db.SaveChangesAsync();

İki parallel request aynı Paid state’ini okuyup authorization’dan geçebilir. Yetkilendirme kararı doğru olsa da operation invariant’ı bozulur. Authorization ile transaction ve concurrency control birlikte ele alınmalıdır.

Örneğin atomic state transition:

var affected = await db.Invoices
    .Where(x =>
        x.Id == invoiceId &&
        x.TenantId == tenantContext.RequiredTenantId &&
        x.Status == InvoiceStatus.Paid)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(
            x => x.Status,
            InvoiceStatus.RefundPending));

if (affected != 1)
{
    return Conflict();
}

Bu örnek remote refund’ın tüm dağıtık transaction sorunlarını çözmez. Ancak aynı state’ten iki operation’ın başlamasını engelleyen başlangıç kontrolünü atomik hâle getirir. Payment provider idempotency, outbox ve reconciliation ayrıca tasarlanmalıdır.

Business rule, workflow ve concurrency ilişkisini kod incelemesinde business logic açıklarının nasıl yakalandığını anlattığımız rehberde daha ayrıntılı ele alıyoruz.

18. Authorization logging neyi kaydetmeli, neyi kaydetmemelidir?

Yetki reddi yalnızca 403 response değildir. Detection, incident investigation ve false denial analizi için anlamlı event üretmelidir.

Örnek event alanları:

  • Timestamp
  • Correlation veya trace ID
  • Actor subject ID
  • Authentication scheme ve issuer
  • Tenant ID
  • Resource type
  • Pseudonymized veya güvenli resource identifier
  • Operation
  • Applied policy
  • Allow veya deny sonucu
  • Deny reason category
  • Entry point
  • Client ve service context
  • Break-glass veya delegated access bilgisi

Password, access token, raw authorization header, full sensitive document veya gereksiz PII log’a yazılmamalıdır.

Deny reason client’a açılmamalıdır

Internal event “resource bulundu fakat tenant mismatch nedeniyle reddedildi” diyebilir. Client response’u bunu açıklamak zorunda değildir. External response için 404 seçilmişse internal telemetry gerçek kararı korumalıdır.

Başarılı privileged operation da loglanmalıdır

Yalnızca denial loglamak eksiktir. Şu başarılı operation’lar yüksek değerli audit event üretmelidir:

  • Cross-tenant access
  • Role ve permission değişikliği
  • User impersonation
  • Bulk export
  • Secret veya key rotation
  • Refund ve approval
  • Query filter bypass
  • Break-glass kullanımı
  • Sensitive field görüntüleme
  • Tenant ownership transfer

Audit log’un kendisi de authorization ile korunmalı ve tamper resistance göz önüne alınmalıdır.

Log sonucu authorization input’u olmamalıdır

“Bu kullanıcı daha önce bu resource’a erişmiş, yine izin ver” gibi audit history tabanlı kısa yollar privilege persistence üretebilir. Yetki kararı current policy ve current state’e göre verilmeli, log kanıt olarak tutulmalıdır.

19. .NET yetkilendirme kaynak kod incelemesi adım adım nasıl yürütülür?

Etkili review, tek tek dosyalarda rastgele gezinmek yerine kanıt zinciri kurar.

Adım 1: Security objective ve invariant’ları çıkarın

Örnek:

  • Tenant verisi tenant sınırını aşamaz
  • Customer yalnızca kendi invoice’larını okuyabilir
  • Refund yalnızca Paid invoice’a ve kalan refundable amount kadar uygulanabilir
  • İşlemi oluşturan actor aynı işlemi final approve edemez
  • Platform support PII görüntülemek için atanmış case ve reason code taşır
  • Cross-tenant export yalnızca auditor capability’siyle çalışır

Adım 2: Identity ve claim üretim hattını izleyin

Authentication scheme, issuer, audience, claim mapping, role claim type, token lifetime, revocation ve session behavior incelenir. Authorization doğru claim’e bakıyor olabilir, fakat claim güvenilmeyen issuer’dan geliyorsa karar yine yanlıştır.

Adım 3: Entry point ve route manifest oluşturun

Controller, Minimal API, Razor Pages, SignalR, gRPC, webhook ve consumer’lar tek listede toplanır. Her entry point’in public veya protected olma gerekçesi yazılır.

Adım 4: Policy graph çıkarın

Policy name’den requirement’lara, requirement’lardan handler’lara gidilir. Aynı requirement için kayıtlı bütün handler’lar bulunur. DefaultPolicy, FallbackPolicy, dynamic provider ve environment override’ları eklenir.

Adım 5: Data access path’ini actor ve tenant ile bağlayın

Her sensitive object lookup için şu sorulur:

  • ID nereden geliyor?
  • Tenant nereden geliyor?
  • Owner veya membership nerede doğrulanıyor?
  • Query filter var mı?
  • Filter devre dışı bırakılabiliyor mu?
  • Projection hangi field’ları çıkarıyor?
  • Cache key tenant’ı içeriyor mu?

Adım 6: Operation-level ve field-level kararı doğrulayın

Read yetkisi update yetkisine dönüşüyor mu? Update yetkisi protected property’leri de kapsıyor mu? Admin role’ü bütün operation’ları gereksiz yere açıyor mu?

Adım 7: Aynı domain action’ın bütün caller’larını bulun

Find Usages, call hierarchy ve code search ile service method’un HTTP, queue, job ve gRPC caller’ları çıkarılır. Kontrolün yalnızca bir caller’da kalıp kalmadığı belirlenir.

Adım 8: Negative ve boundary testleri çalıştırın

Farklı actor, tenant, owner, role, claim issuer, resource state ve entry point kombinasyonları denenir. Authorization sonucu mutation’a kadar korunuyor mu test edilir.

Adım 9: Finding’i root cause üzerinden raporlayın

“Yetkilendirme eksik” tek başına yetersizdir. Hangi entry point, hangi actor, hangi resource, hangi operation, hangi missing control ve hangi business impact açıkça yazılmalıdır.

Adım 10: Pattern expansion ve retest yapın

Bir FindAsync(id) sonrası ownership eksikliği bulunduğunda yalnızca tek endpoint düzeltilmez. Aynı repository, DTO ve service pattern’i codebase genelinde aranır. Retest hem bildirilen PoC’yi hem benzer caller’ları hem de regression test’i doğrular.

Kaynak kod analizi ile runtime testin ürettiği kanıtları ve kör noktaları Kaynak Kod Analizi ile Sızma Testi Nerede Ayrışır? yazısında karşılaştırıyoruz.

20. Yetkilendirme bulgusu nasıl raporlanmalıdır?

İyi rapor yalnızca riskli satırı göstermez. Authorization kararının neden yanlış olduğunu yeniden üretilebilir biçimde açıklar.

Örnek finding yapısı

Başlık: Cross-tenant invoice export nedeniyle yetkisiz finansal veri erişimi

Etkilenen component: Billing.Api, InvoiceExportEndpoints.cs

Entry point: POST /api/reports/invoices/export

Ön koşul: Herhangi bir tenant’ta FinanceViewer permission’ına sahip authenticated user

Root cause: Endpoint, request body’den gelen tenantId değerini export query’sinde doğrudan kullanıyor. Policy kullanıcının genel FinanceViewer permission’ını doğruluyor fakat seçilen tenant ile membership ilişkisini doğrulamıyor.

Attack path:

  1. 1Tenant A kullanıcısı geçerli token alır.
  2. 2Kendi tenant ID’siyle export request gönderir.
  3. 3Request body’deki tenantId değerini Tenant B ID’siyle değiştirir.
  4. 4Uygulama Tenant B invoice kayıtlarını export eder.

Etkisi: Tenant’lar arası finansal veri ihlali, müşteri gizliliğinin bozulması ve toplu veri exfiltration.

Kanıt: Request-response, ilgili code path, query ve policy registration.

Remediation: Tenant’ı request’ten authority olarak alma. Server-side doğrulanmış ITenantContext kullan. Cross-tenant export gerekiyorsa ayrı platform policy, explicit audit ve dar service tanımla. Object ve field-level projection uygula.

Regression test: Tenant A actor’ı ile Tenant B ID’si kullanıldığında 404 veya 403 bekle. Aynı testi bulk, scheduled export ve gRPC caller için tekrar et.

CVSS tek başına business risk değildir

CVSS teknik severity için kullanılabilir. Buna ek olarak:

  • Erişilebilen tenant sayısı
  • Tekil kayıt mı, bulk export mu olduğu
  • Verinin niteliği
  • Gerekli mevcut permission
  • Exploit’in loglanıp loglanmadığı
  • Privileged operation’ın geri alınabilirliği
  • Finansal veya operasyonel etki

ayrı yazılmalıdır.

Remediation “her yere [Authorize] ekleyin” olmamalıdır

Object-level authorization açığının çözümü yalnızca [Authorize] değildir. Kalıcı düzeltme şu katmanlardan uygun olanlarını içerebilir:

  • Authenticated-by-default fallback policy
  • Coarse endpoint policy
  • Server-side tenant context
  • Tenant-bound query
  • Resource-based authorization handler
  • Operation-specific requirement
  • Field-specific DTO ve projection
  • Domain invariant
  • Transaction ve concurrency control
  • Audit event
  • Negative regression test

21. .NET authorization Secure Code Review kontrol listesi

Authentication ve identity

  • [ ] Kabul edilen authentication scheme’ler açıkça tanımlı mı?
  • [ ] Issuer ve audience doğrulanıyor mu?
  • [ ] Role ve permission claim’lerinin kaynağı güvenilir mi?
  • [ ] RoleClaimType ve NameClaimType mapping’i doğrulandı mı?
  • [ ] Aynı claim farklı issuer’larda farklı anlam taşıyor mu?
  • [ ] Privilege değişikliği token ve session’lara ne zaman yansıyor?

Global configuration

  • [ ] DefaultPolicy beklenen requirement’ları içeriyor mu?
  • [ ] FallbackPolicy yeni endpoint’leri authenticated-by-default koruyor mu?
  • [ ] UseAuthentication ve UseAuthorization doğru sırada mı?
  • [ ] ASP0001 ve ASP0026 gibi analyzer uyarıları etkin mi?
  • [ ] Environment’a göre permissive policy açılıyor mu?
  • [ ] Custom policy provider fail-closed davranıyor mu?

Endpoint metadata

  • [ ] Bütün controller ve Minimal API route’ları envantere alındı mı?
  • [ ] [AllowAnonymous] kullanımlarının public gerekçesi var mı?
  • [ ] Base class ve inherited metadata incelendi mi?
  • [ ] Route group policy’leri child endpoint’lerde doğrulandı mı?
  • [ ] Aynı handler’ın bütün route registration’ları bulundu mu?
  • [ ] Razor Pages folder ve page convention precedence doğru mu?

Role, claim ve policy

  • [ ] Comma-separated role listesinde OR semantics kasıtlı mı?
  • [ ] Birden fazla attribute’un AND semantics’i kasıtlı mı?
  • [ ] Claim yalnızca type ile değil gerekli value ve issuer ile değerlendiriliyor mu?
  • [ ] Policy içindeki bütün requirement’lar doğru mu?
  • [ ] Aynı requirement için bütün handler’lar birlikte incelendi mi?
  • [ ] context.Fail() ve context.Succeed() semantics’i doğru kullanılıyor mu?
  • [ ] Role, tenant scope’u ve operation scope’u gereğinden geniş mi?

Object ve tenant authorization

  • [ ] Object lookup actor veya tenant ile bağlanıyor mu?
  • [ ] Request’teki userId ve tenantId authority olarak kullanılmıyor mu?
  • [ ] IAuthorizationService resource ile çağrılıyor mu?
  • [ ] Read, update, delete, approve ve refund ayrı operation’lar mı?
  • [ ] Parent-child object ilişkisi doğrulanıyor mu?
  • [ ] Cross-tenant administrator bypass’ı explicit ve audit edilebilir mi?
  • [ ] 404 ve 403 kararı internal audit bilgisini kaybetmiyor mu?

EF Core ve data access

  • [ ] Global query filter tenant isolation için doğru context’i kullanıyor mu?
  • [ ] Birden fazla HasQueryFilter yanlışlıkla overwrite ediliyor mu?
  • [ ] IgnoreQueryFilters() call site’leri tek tek incelendi mi?
  • [ ] Raw SQL, Dapper veya başka ORM path’leri aynı tenant kontrolünü uyguluyor mu?
  • [ ] Pooled context veya factory yanlış tenant capture ediyor mu?
  • [ ] Tenant context yokken query fail-open çalışıyor mu?
  • [ ] Cache key tenant, actor ve permission context’ini içeriyor mu?

HTTP dışı girişler

  • [ ] SignalR hub ve method’ları protected mı?
  • [ ] SignalR group membership resource authorization’dan geçiyor mu?
  • [ ] Long-lived connection privilege revocation davranışı tanımlı mı?
  • [ ] gRPC service ve method policy’leri var mı?
  • [ ] Queue consumer ve background job authority’si explicit mi?
  • [ ] Webhook authentication ile business authorization ayrılmış mı?
  • [ ] Controller policy’sini bypass eden service caller var mı?

Field ve business context

  • [ ] Update DTO yalnızca actor’ın değiştirebileceği field’ları içeriyor mu?
  • [ ] Generic mapper protected property’leri yazıyor mu?
  • [ ] JSON Patch path allowlist’i var mı?
  • [ ] Response projection hassas field’ları dışlıyor mu?
  • [ ] Maker-checker, state ve limit kuralları role kontrolünden ayrı uygulanıyor mu?
  • [ ] Authorization ile mutation arasında TOCTOU riski var mı?

Test, log ve remediation

  • [ ] Actor-resource-operation matrix oluşturuldu mu?
  • [ ] Başka owner ve başka tenant negatif testleri var mı?
  • [ ] Missing, duplicate ve yanlış issuer claim testleri var mı?
  • [ ] Aynı operation’ın bütün entry point’leri test edildi mi?
  • [ ] Denied ve privileged allowed operation audit ediliyor mu?
  • [ ] Log’lar token veya hassas PII içermiyor mu?
  • [ ] Düzeltme aynı root cause pattern’ine yayıldı mı?
  • [ ] Retest production’a gidecek aynı build veya commit üzerinde yapıldı mı?

23. Sonuç: Yetkilendirme attribute değil, uçtan uca karar zinciridir

ASP.NET Core güçlü authorization primitives sunar. [Authorize], policy, requirement, handler ve IAuthorizationService doğru kullanıldığında okunabilir ve test edilebilir bir model kurulabilir. Buna karşılık framework güvenlik kararını ürün adına tanımlamaz.

Kaynak kod incelemesinde şu zincir eksiksiz izlenmelidir:

  1. 1Request veya event hangi entry point’ten geliyor?
  2. 2Hangi authentication scheme actor’ı oluşturuyor?
  3. 3Hangi claim ve role’ler hangi issuer’dan geliyor?
  4. 4Endpoint hangi default, fallback veya named policy’yi alıyor?
  5. 5Policy hangi requirement ve handler’ları çalıştırıyor?
  6. 6Talep edilen resource hangi tenant ve owner’a ait?
  7. 7Actor bu resource üzerinde tam olarak hangi operation’ı istiyor?
  8. 8Resource state’i ve business context kararı değiştiriyor mu?
  9. 9Aynı domain action’a başka caller ulaşabiliyor mu?
  10. 10Karar mutation anına kadar transaction ve concurrency altında korunuyor mu?

Bu sorulardan biri cevapsızsa controller üzerinde [Authorize] bulunması güvence sağlamaz.

Güçlü .NET yetkilendirme tasarımı genellikle tek katmana dayanmaz. Authenticated-by-default configuration yanlışlıkla public endpoint oluşmasını azaltır. Coarse policy genel capability’yi doğrular. Server-side tenant context veri sınırını belirler. Tenant-bound query gereksiz cross-tenant object loading’i engeller. Resource-based handler actor, resource ve operation ilişkisini değerlendirir. Domain service state ve business invariant’ı korur. Negative testler modelin yalnızca beklenen actor için çalıştığını kanıtlar.

Otomatik analiz bu zincirde coverage sağlar. Framework analyzer’ları metadata precedence ve middleware order hatalarını bulabilir. Custom SAST rule’ları request kaynaklı tenant ID, ownership’siz lookup ve broad DTO gibi pattern’leri işaretleyebilir. Uzman Secure Code Review ise bu sinyalleri gerçek role modeli, tenant sınırı, object ilişkisi ve business impact ile doğrular.

SECNODEX’in Kaynak Kod Analizi ve Secure Code Review hizmeti, otomatik SAST coverage’ını uzman manuel inceleme, authorization ve business logic analizi, root cause odaklı raporlama ve retest ile birleştirir.

Kodda görülen yetkilendirme zafiyetlerinin çalışan uygulama üzerindeki gerçek exploitability’sini ve etkisini doğrulamak için Sızma Testi hizmetimizi inceleyebilirsiniz.

.NET uygulamanızdaki controller, Minimal API, Razor Pages, SignalR, gRPC ve background processing yüzeyleri için doğru inceleme kapsamını belirlemek üzere SECNODEX ile iletişime geçebilirsiniz.

Kaynaklar ve ileri okuma

<!--

EDİTORYAL INTERNAL LINK NOTU

Bu içerik Kaynak Kod Analizi pillar'ının .NET authorization cluster yazısıdır.

Ana pillar:

  • /blog/kaynak-kod-analizi-nedir-kapsamli-rehber

Destek içerikleri:

  • /blog/idor-acigi-neden-hala-bu-kadar-yaygin
  • /blog/kod-incelemesinde-business-logic-aciklari-nasil-yakalanir
  • /blog/sast-nedir-hangi-aciklari-bulur-hangilerini-kacirir
  • /blog/secure-code-review-ile-otomatik-sast-taramasi-farki
  • /blog/kaynak-kod-analizi-ile-sizma-testi-farki

Bu cluster yazısından pillar'a ilk üç bölüm içinde doğal bağlantı verilmelidir.

Pillar içindeki "Dil/framework'e göre yaklaşım" ve "Hangi zafiyetler kod düzeyinde yakalanır?" bölümlerinden bu yazıya geri link verilmelidir.

Ana hizmet bağlantısı /hizmetler/kaynak-kod-analizi olmalıdır.

FAQPage schema yalnızca sayfada görünen 12 soru-cevapla birebir eşleşmelidir.

-->

#.NET Yetkilendirme#ASP.NET Core Authorization#Secure Code Review#Kaynak Kod Analizi#Policy-Based Authorization#Resource-Based Authorization#IDOR#Multi-Tenant Security

Sık sorulan sorular

.NET yetkilendirme nedir?

.NET yetkilendirme, authenticated veya belirli durumlarda anonymous actor’ın bir resource üzerinde belirli operation’ı yapıp yapamayacağını değerlendiren kontrollerdir. ASP.NET Core role, claim, policy ve resource-based authorization modellerini sağlar.

`[Authorize]` kullanmak yeterli midir?

Hayır. [Authorize] endpoint’e erişim için coarse-grained kontrol sağlar. Belirli object’in owner’ı, tenant’ı, state’i veya istenen operation gibi bilgiler için resource-based ve business-level authorization gerekir.

Authentication ile authorization arasındaki fark nedir?

Authentication actor’ın kimliğini doğrular. Authorization bu actor’ın talep edilen işlemi yapıp yapamayacağına karar verir. Geçerli JWT ile sisteme girmek başka müşterinin verisini görme yetkisi vermez.

`DefaultPolicy` ile `FallbackPolicy` arasındaki fark nedir?

DefaultPolicy, policy adı belirtilmeyen [Authorize] veya RequireAuthorization() için kullanılır. FallbackPolicy, endpoint üzerinde authorization metadata’sı bulunmadığında devreye girer.

`[AllowAnonymous]`, `[Authorize]` kullanımını etkisizleştirir mi?

Evet. Endpoint metadata’sında AllowAnonymous varsa authorization check’leri bypass edilir. Base class, folder convention veya route registration’dan gelen anonymous metadata da beklenmeyen açıklık oluşturabilir.

`[Authorize(Roles = "Admin,Manager")]` AND mi, OR mu çalışır?

OR çalışır. Kullanıcının Admin veya Manager role’lerinden birine sahip olması yeterlidir. İki ayrı [Authorize(Roles = "...")] attribute’u kullanılırsa bütün role requirement’larının karşılanması gerekir.

Resource-based authorization ne zaman gerekir?

Karar belirli bir database row, document, tenant, invoice, case veya başka domain object’in owner, tenant, state ve operation bilgisine bağlıysa gerekir. Resource önce yüklenir, ardından IAuthorizationService.AuthorizeAsync ile değerlendirilir.

EF Core global query filter tenant güvenliğini tek başına sağlar mı?

Hayır. Güçlü bir defense-in-depth katmanıdır fakat operation permission’ını değerlendirmez. IgnoreQueryFilters, raw SQL, başka context ve yanlış tenant capture gibi bypass yolları ayrıca incelenmelidir.

IDOR .NET kaynak kodunda nasıl anlaşılır?

User-controlled ID ile object yüklenip current actor, owner veya tenant ilişkisi doğrulanmadan response veya mutation üretilmesi önemli sinyaldir. FindAsync(id) veya SingleOrDefaultAsync(x => x.Id == id) sonrasında object-level control aranmalıdır.

SAST .NET authorization açıklarını bulabilir mi?

Bazı pattern’leri bulabilir. AllowAnonymous çakışması, middleware order, request tabanlı tenant query, IgnoreQueryFilters ve eksik metadata güçlü sinyallerdir. Ancak role’ün business açısından doğru olup olmadığını ve actor-resource ilişkisini çoğu zaman uzman review tamamlar.

SignalR ve gRPC için ayrıca authorization gerekir mi?

Evet. Hub, hub method’u, gRPC service ve service method’u ayrı entry point’lerdir. Authentication uygulanmadığında varsayılan olarak anonymous erişime açık olabilirler. Resource ve tenant kontrolü de method içinde ayrıca gerekebilir.

.NET yetkilendirme incelemesi ne zaman yapılmalıdır?

Yeni identity provider, tenant modeli, administrator function’ı, role veya permission değişikliği, büyük API release’i, SignalR/gRPC eklenmesi ve authorization incident’i sonrasında yapılmalıdır. Kritik authorization kodu değiştikçe pull request seviyesinde otomatik ve manuel review uygulanmalıdır.

Siber güvenlik çalışmanızı
SECNODEX ile planlayın

İhtiyacınız tek bir uygulamanın testinden kurum genelinde bir Red Team çalışmasına kadar uzanabilir. Ekibimiz scope’u, kritik varlıkları ve beklenen çıktıları sizinle netleştirir. Ardından iş hedefinize ve risk önceliklerinize uygun, sınırları belirlenmiş bir çalışma planı sunar.