Mert Özen Her Satırda Daha İleri
Güvenlik Threat Modeling ve Güvenli Geliştirme Süreci — #02

STRIDE ile Tehdit Bulmak ve Önceliklendirmek: Element Başına STRIDE, Attack Tree, Risk Puanlama ve Mitigation Kararı

Mert Özen 15 Kas 2026 11 dk 5 görüntülenme
STRIDE ile Tehdit Bulmak ve Önceliklendirmek: Element Başına STRIDE, Attack Tree, Risk Puanlama ve Mitigation Kararı

STRIDE'ı DFD'nin her elemanına türüne göre uygulayıp tehditleri olasılık × etki ile sıralamak, her biri için mitigate, eliminate, transfer ya da accept kararı vermek ve kararı bir teste ya da risk register satırına bağlamak gerekir. Bookstore'un webhook'u ve kurumsal satın alma onayı üzerinde LINDDUN, attack tree, DREAD'in neden bırakıldığı ve dört göz kuralını doğrulayan xUnit testleri, gerçek çıktılarla.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; testler xUnit v3 4.0.3 ile çalıştırılmış, model Threat Dragon 2.6.2'de açılıp şemasına göre doğrulanmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) okullara ve kütüphanelere toplu satış yapıyor: Kurumsal Sipariş'te en az 20 kitap alınıyor ve %10 kurumsal indirim uygulanıyor. Lakeside School'un kütüphanesi için onay limiti 5.000 TL; limiti aşan bir satın alma isteğini InstitutionApprover rolündeki biri onaylamadan sipariş oluşmuyor. Kodda onay endpoint'i şu kontrolü yapıyor: çağıran kullanıcı aynı kurumda mı, rolü InstitutionApprover mı?

Okulun bilgi işlem sorumlusu Linus'a, işler hızlansın diye hem InstitutionBuyer hem InstitutionApprover rolü verilmiş. Tahmin et: Linus 7.002 TL'lik bir istek oluşturup onu kendisi onaylayabilir mi? Kontrol listesine göre evet; iki koşul da sağlanıyor. Bu bir kod hatası değil, eksik bir kural: dört göz prensibi (bir işlemi yapanla onaylayanın farklı kişiler olması) hiçbir yerde yazılmamış. Bu yazıda böyle tehditleri sistematik olarak bulmayı, sıralamayı ve her birini bir teste ya da kayıtlı bir karara dönüştürmeyi konuşuyoruz.

STRIDE'ı Elemanlara Uygulamak

STRIDE'ın (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege; altı tehdit türünün baş harfleri) altı harfi Web Güvenliği (OWASP Top 10) serisinin Insecure Design yazısında anlatıldı. Orada bir soru listesi olarak kullandık; burada aynı listeyi ilk yazıdaki DFD'nin (Data Flow Diagram; verinin hareketini gösteren diyagram) her elemanına uyguluyoruz. Bu yaklaşımın adı STRIDE per element. Fikir şu: her eleman türü her tehdide aynı ölçüde açık değildir, o yüzden her elemanda altı sorunun hepsini değil, o türe uyanları sorarsın.

Eleman türüÖncelikli STRIDE sorularıBookstore'da sorduğumuz
External entitySpoofing, RepudiationWebhook'u gönderen gerçekten ödeme sağlayıcı mı?
ProcessAltısı daInstitutional onayı kim verebilir, kendi isteğini onaylayan olur mu?
Data storeTampering, Information disclosure, Denial of service; log ise RepudiationBookstoreDb yedekleri kimin elinde?
Data flowTampering, Information disclosure, Denial of serviceKargo firmasına giden veri gerekenden fazla mı?

Oturumda Emre diyagramı ekrana yansıttı ve ekip her elemanda tabloya bakarak ilerledi. Webhook flow'unda Spoofing sorusu hemen cevabı belli bir tehdit çıkardı: internetten gelen bir payment.succeeded isteği imzası doğrulanmadan siparişi Paid yapıyorsa, herkes istediği siparişi ödenmiş sayabilir. Aynı flow'da Tampering sorusu replay'i, yani yakalanmış geçerli bir isteğin tekrar gönderilmesini getirdi. Payments process'inde Repudiation sorusu, destek ekibinin "bu durum değişikliği sağlayıcıdan mı geldi, back office'ten mi?" sorusuna cevap veremediğini gösterdi.

Bulunan her tehdit Threat Dragon'da ilgili elemana yazıldı: başlık, STRIDE türü, açıklama, mitigation, durum ve puan. İkinci diyagram olarak kurumsal satın alma onayı eklendi. Model JSON olduğu için tehdit listesini jq ile çıkarmak backlog'la karşılaştırmayı kolaylaştırıyor:

jq -r '[.detail.diagrams[].cells[] | .data.name as $e | .data.threats[]? | {n: .number, e: ($e | gsub("\n"; " ")), status, severity, score, type}] | sort_by(.n)[] | [.n, .status, .severity, .score, .type, .e] | @tsv' threat-model/bookstore-checkout.json
1	Mitigated	High	L2 x I3 = 6	Tampering	1 POST /api/orders
2	Mitigated	Medium	L2 x I2 = 4	Tampering	Ordering
3	Open	Medium	L2 x I2 = 4	Denial of service	Ordering
4	Open	High	L2 x I3 = 6	Spoofing	7 webhook payment.succeeded
5	Open	Low	L1 x I2 = 2	Tampering	Payments
6	Open	Low	L1 x I2 = 2	Repudiation	Payments
7	Open	Medium	L1 x I3 = 3	Information disclosure	BookstoreDb
8	Open	Low	L2 x I1 = 2	Information disclosure	9 create shipment
9	Mitigated	High	L2 x I3 = 6	Elevation of privilege	Institutional
10	Mitigated	High	L2 x I3 = 6	Tampering	Institutional
11	Mitigated	High	L2 x I3 = 6	Elevation of privilege	Institutional
12	Open	Medium	L2 x I2 = 4	Elevation of privilege	Institutional

Tablo bir kontrol listesi değil, bir başlangıç noktası. "Öncelikli" sütunundaki harfler dışındaki bir tehdit aklına geliyorsa yaz; STRIDE per element'in amacı unutulan soruları hatırlatmaktır, sorulabilecek soruları sınırlamak değil.

LINDDUN ile Privacy Tehditleri

STRIDE güvenlik tehditlerini sorar; kişisel veriyle ilgili riskleri ise tam yakalamaz. Bunun için LINDDUN (yedi privacy tehdit türünün baş harfleri) kullanılır: Linking, Identifying, Non-repudiation, Detecting, Data Disclosure, Unawareness & Unintervenability ve Non-compliance. LINDDUN da DFD üzerinde çalışır; aynı diyagramı bir de bu gözle okursun. Bookstore'da iki soru öne çıktı. Kargo firmasına giden flow'da Data Disclosure: firma sipariş içeriğini ve e-posta adresini görüyor, ihtiyacı yok (T08). Ordering'de Linking: kurumsal kullanıcıların sipariş geçmişi bireysel hesaplarıyla aynı e-posta üzerinden birleştirilebilir mi? İkinci soru KVKK (Kişisel Verilerin Korunması Kanunu) ile doğrudan ilgili olduğu için ayrıntısı Veri Gizliliği ve KVKK serisinin kişisel veri envanteri yazısında; burada yalnız modelde not olarak duruyor.

Attack Tree ile Kurumsal Onayı Atlatmak

STRIDE eleman eleman ilerler; attack tree ise bir saldırganın hedefinden geriye doğru gider. Kök düğüm hedeftir, dallar o hedefe giden alternatif yollardır (OR) ya da birlikte gereken adımlardır (AND). Kurumsal onay için ekip şu ağacı çıkardı; numaralar modeldeki tehditlerdir:

GOAL: an order above the unit limit is placed without a valid approval
  OR
  +-- approve it yourself (T09)
  |     +-- a user with both roles approves own request
  |     +-- an approver of another institution approves it
  +-- skip the approval step (T11)
  |     +-- call POST .../purchase-requests/{id}/order while PendingApproval
  +-- change the request after approval (T10)
  |     +-- PUT .../lines after approval, then order
  +-- stay under the limit (T12)
        +-- split one large request into several small ones

Ağacın faydası, STRIDE listesinde ayrı ayrı duran tehditlerin aynı hedefe hizmet ettiğini göstermesi. T12 ilk bakışta "kural gereği izin verilen bir şey" gibi duruyor; ağaçta ise diğer üç yolla aynı hedefe gidiyor. Bu, ona neden bir karar yazılması gerektiğini de açıklıyor.

Önceliklendirmek: Olasılık × Etki

On iki tehdidin hepsini aynı sprint'te kapatamazsın. Bookstore ekibi basit bir puanlama kullanıyor: olasılık 1–3 (1: özel bilgi ve erişim gerektirir, 3: internetten herkes deneyebilir) çarpı etki 1–3 (1: tek kullanıcıyı rahatsız eder, 3: para kaybı ya da başka müşterinin verisi). 6–9 High, 3–5 Medium, 1–2 Low. Puan, modeldeki her tehdidin score alanında L2 x I3 = 6 biçiminde duruyor; böylece "neden High?" sorusunun cevabı tehditle birlikte okunuyor.

Daha ayrıntılı bir formül kullanmak cazip gelebilir. DREAD (Damage, Reproducibility, Exploitability, Affected users, Discoverability; beş kriterle puanlama) bunun en bilinen örneği. Ama beş kriterin her biri kişiden kişiye değişen bir tahmin ve sonuçta bir sayı ortaya çıkıyor gibi görünse de kesinlik artmıyor. Adam Shostack'ın 2018 tarihli bir yazısına göre bu puanlar üzerindeki anlaşmazlıklar Microsoft'ta tekrar tekrar yaşanmış ve ekip "bug bar" denen, önceden tanımlı eşiklerle karar veren bir yaklaşıma geçmiş. Bizim 3×3 matrisimiz de aslında küçük bir bug bar: amaç doğru sayıyı bulmak değil, ekibin aynı dili konuşması. CVSS (Common Vulnerability Scoring System; bilinen zafiyetler için sektör puanlama sistemi) ise tasarım tehdidi için değil, bulunmuş bir zafiyet için kullanılır; dependency zafiyetlerinde CVSS ve EPSS (bir zafiyetin istismar edilme olasılığı tahmini) ile önceliklendirme DevSecOps serisinin dependency scanning yazısında.

Mitigation Kararı: Dört Seçenek

Her tehdit için dört karardan biri verilir ve hepsi geçerlidir. Mitigate: bir kontrol eklersin; T04 için webhook imzasını ve zaman damgasını doğrulamak, sonra ödeme durumunu sağlayıcının API'sinden tekrar okumak. Eliminate: tehdidi doğuran özelliği tasarımdan çıkarırsın; T01'de fiyatın istek gövdesinde hiç taşınmaması böyle bir karar, fiyatı manipüle edilecek bir alan yok. Transfer: riski onu daha iyi yöneten tarafa bırakırsın; kart bilgisinin sağlayıcının sayfasında girilmesi (ilk yazıdaki 6 numaralı flow) tam olarak bu. Accept: riski bilerek kabul edersin.

Kabul en çok yanlış kullanılan karardır, çünkü "hiçbir şey yapmamak" ile karışır. Fark, kararın yazılı olmasıdır: kim kabul etti, neden, ne zaman tekrar bakılacak. Bookstore'da kabul edilen her risk threat-model/risk-register.md dosyasında bir satır:

| Id | Threat | Decision | Why | Owner | Review |
|---|---|---|---|---|---|
| R-01 | T07 Customer data readable from backups | Accept residual risk | Backups are encrypted and the storage account allows only the backup identity; the rest needs a second key management system | Emre | 2027-05-01 |
| R-02 | T12 Splitting a request to stay under the approval limit | Accept for release 1 | Each split request is still under the unit's limit and visible in the daily report; the rolling limit needs a product decision | Selin | 2027-03-01 |

R-02'nin sahibi Selin, çünkü kural bir ürün kararı: bir birimin günlük toplamına limit koymak kurumların satın alma alışkanlığını değiştirir ve bunu geliştirici tek başına seçemez. Tarihi geçen satır backlog'a geri döner.

Tehdidi Teste Çevirmek

Bir tehdidi "Mitigated" yapmanın tek koşulu, onu doğrulayan bir testin olması ve geçmesi. Linus senaryosu (T09) için önce testi yazdık. Test, kodun içinden değil HTTP üzerinden gidiyor; kullanıcı kimliğini yalnız test projesinde bulunan bir authentication handler'dan alıyor, API'ye hiçbir test kodu girmiyor:

[Fact]
[Trait("threat", "T09")]
public async Task Buyer_with_both_roles_cannot_approve_own_request()
{
    HttpClient linus = factory.CreateClientAs(TestUsers.Linus);
    PurchaseRequestView request = await CreateAsync(linus, (Middlemarch, 20));

    HttpResponseMessage approve = await linus.PostAsync($"/api/institutions/me/purchase-requests/{request.Id}/approve", null, ct);

    Assert.Equal(HttpStatusCode.Forbidden, approve.StatusCode);
    ProblemDetails? problem = await approve.Content.ReadFromJsonAsync<ProblemDetails>(ct);
    Assert.Equal("The requester cannot approve their own request.", problem?.Detail);
}

Trait testi modeldeki tehdide bağlıyor; o tehdidin bütün testlerini tek komutla çalıştırabiliyorsun. Kural henüz kodda yokken çıktı şöyleydi:

dotnet test --filter-trait "threat=T09"
failed Bookstore.Api.Tests.PurchaseApprovalTests.Buyer_with_both_roles_cannot_approve_own_request (2ms)
  Assert.Equal() Failure: Values differ
  Expected: Forbidden
  Actual:   OK
Test run summary: Failed!
  total: 3
  failed: 1
  succeeded: 2

OK, yani Linus kendi isteğini onayladı. Düzeltme PurchaseRequestService.ApproveAsync içinde tek bir kural; önemli olan, kuralın rol kontrolünden sonra ve durumdan bağımsız olarak çalışması:

public async Task<PurchaseOutcome> ApproveAsync(InstitutionalCaller caller, int requestId, CancellationToken ct)
{
    PurchaseRequest? request = await FindAsync(caller, requestId, ct);
    if (request is null)
    {
        return new PurchaseOutcome.NotFound();
    }
    if (!caller.IsApprover)
    {
        return new PurchaseOutcome.Forbidden("Only an institution approver can approve.");
    }
    // Dört göz kuralı: iki role sahip olmak kendi isteğini onaylamaya yetmez (tehdit T09).
    if (request.RequestedBy == caller.UserId)
    {
        return new PurchaseOutcome.Forbidden("The requester cannot approve their own request.");
    }
    if (request.Status != PurchaseRequestStatus.PendingApproval)
    {
        return new PurchaseOutcome.Conflict($"The request is {request.Status}, not PendingApproval.");
    }
    request.Status = PurchaseRequestStatus.Approved;
    request.ApprovedBy = caller.UserId;
    request.ApprovedAtUtc = clock.GetUtcNow().UtcDateTime;
    request.ApprovedLinesHash = request.ComputeLinesHash();
    await db.SaveChangesAsync(ct);
    return new PurchaseOutcome.Done(request);
}

Başka kurumun approver'ı (Alan) için FindAsync zaten isteği kurum id'siyle filtreliyor ve 404 dönüyor; isteğin var olduğunu bile söylemiyoruz. Onaydan sonra satır değiştirme tehdidi (T10) için iki katman var: satırlar değişince istek tekrar PendingApproval durumuna dönüyor, sipariş anında da onaylanan satırların hash'i (SHA-256) şimdiki satırlarla karşılaştırılıyor. İkinci kontrol, ileride satırları değiştiren başka bir yol (örneğin back office'ten düzeltme) eklenirse devreye giriyor:

// Durum sunucuda kontrol edilir; istemci onay adımını atlayamaz (tehdit T11).
if (request.Status != PurchaseRequestStatus.Approved)
{
    return new PurchaseOutcome.Conflict($"The request is {request.Status}; only an approved request can be ordered.");
}
// Derinlemesine savunma: satırlar hâlâ onaylananlar olmalı (tehdit T10).
if (request.ApprovedLinesHash != request.ComputeLinesHash())
{
    return new PurchaseOutcome.Conflict("The request changed after approval.");
}

Kural eklendikten sonra aynı komut ve bütün test suite'i:

Test run summary: Passed!
  total: 3
  failed: 0
  succeeded: 3
  skipped: 0
  duration: 1s 457ms
Test run summary: Passed!
  total: 16
  failed: 0
  succeeded: 16
  skipped: 0
  duration: 1s 278ms

Ancak bundan sonra T09, T10 ve T11 modelde Mitigated yapıldı ve mitigation alanına testin adı yazıldı. Webhook imzası (T04) ise henüz Open; backlog'da SEC-11 olarak duruyor ve kendi testiyle kapanacak.

Üretimde Dikkat

Tehdit numaraları sabit kalsın. T09 bir kez verildiyse silinen bir tehdidin numarası başka bir tehdide verilmez; test Trait'leri, backlog item'ları ve risk register bu numaralara bağlı. Modeldeki threatTop alanı bir sonraki numarayı tutuyor.

Kabul edilen risk izlenmeli. T12 için kabul kararı "günlük raporda görünür" varsayımına dayanıyor; o rapor yoksa karar boşa düşer. Bookstore'da her birimin günlük kurumsal sipariş toplamı bir metrik olarak izleniyor ve olağan dışı artış bir alarm üretiyor. Bunun nasıl kurulacağı ASP.NET Core Güvenliği serisinin güvenlik olaylarını gözlemleme yazısında.

Onay gibi iş kuralları için yazdığın testler, sistemin en değerli güvenlik testleridir; çünkü hiçbir tarayıcı onları senin yerine bulamaz. DAST (çalışan uygulamayı dışarıdan test eden güvenlik taraması) bir endpoint'e yetkisiz istek atmayı dener ama "aynı kişinin iki rolü olunca ne olur?" sorusunu sormaz.

Sık Düşülen Tuzaklar

Her tehdide "önüne bir güvenlik duvarı koyarız" diye cevap vermek en yaygın kaçış. WAF (Web Application Firewall; HTTP trafiğini kurallara göre süzen güvenlik katmanı) bilinen saldırı kalıplarını engelleyebilir ama Linus'un isteği tamamen geçerli bir HTTP isteği; içinde engellenecek bir şey yok. İş mantığı tehditlerinin mitigation'ı iş mantığının içinde olur.

İkinci tuzak, mitigation'ı test yazmadan kapatmak. "Kontrol eklendi" diye Mitigated işaretlenen bir tehdit, bir refactoring'de kontrol sessizce silindiğinde kimseye haber vermez. Testin Trait'i ile modeldeki tehdit numarası aynı olunca bu ilişki koddan okunur.

Üçüncüsü puanlamaya fazla zaman harcamak. "Bu 4 mü, 6 mı?" tartışması on dakikayı geçiyorsa yüksek olanı yaz ve devam et; sıralama sprint planlamasında zaten yeniden konuşulacak. Dördüncüsü de bulunan tehditlerin modelde kalıp backlog'a hiç girmemesi: backlog'da olmayan bir tehdit, kimsenin işi değildir.

Küçük Bir Deneme

★☆☆ Modeldeki T05'i (webhook replay) ele al: hangi bilgiyle "bu event daha önce işlendi" diyebilirsin? Bunu bir unique index ve bir integration testiyle nasıl doğrularsın?

★★☆ T12 için risk register satırının yerine bir mitigation yaz: bir birimin son 24 saatteki onaysız toplamı limiti aşıyorsa yeni istek PendingApproval olsun. Önce testi yaz, kırmızı gör, sonra kuralı ekle.

★★★ Shipping process'ine LINDDUN'un yedi türünü uygula. Hangileri Bookstore için gerçek bir tehdit, hangileri değil? Gerçek olanları Threat Dragon'da modele ekle ve JSON diff'ini incele.

Artık tehditlerimiz ve kararlarımız var; ama "webhook imzasını doğrula" gibi bir karar, bir sonraki feature'da tekrar unutulabilir. Serinin üçüncü yazısında bu kararları OWASP ASVS (Application Security Verification Standard; uygulama güvenliği doğrulama standardı) 5.0 ile yazılı güvenlik requirement'larına çeviriyor, user story'lerin acceptance criteria'sına ve Definition of Done'a bağlıyoruz.