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

Kendi Ürünün için Zafiyet Yönetimi: security.txt, Coordinated Disclosure, GitHub Security Advisory ve CVE

Mert Özen 15 Kas 2026 10 dk 4 görüntülenme
Kendi Ürünün için Zafiyet Yönetimi: security.txt, Coordinated Disclosure, GitHub Security Advisory ve CVE

Dışarıdan gelen zafiyet bildirimi için önce bir kapı gerekir: RFC 9116'ya uygun security.txt, SECURITY.md ve GitHub private vulnerability reporting; sonra triage, draft advisory'de gizli düzeltme, CVE talebi ve düzeltilmiş sürümle aynı gün yayın. Bookstore API'sinden security.txt sunmak ve testlemek, sectxt doğrulaması, Bookstore.Contracts NuGet paketi için advisory akışı, bug bounty ve safe harbor.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; security.txt dosyası sectxt 0.9.8 ile doğrulanmıştır. GitHub'daki advisory ve kimlik talebi adımları GitHub dokümantasyonundan anlatılmış, gerçek bir repoda çalıştırılmamıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı), Versioning ve Release Management serisinde Bookstore.Contracts adlı bir NuGet paketi yayımladı; kurumsal müşterilerin entegrasyon ekipleri bu paketi kullanıyor. Bir güvenlik araştırmacısı paketin ISBN (kitapların uluslararası standart numarası) doğrulamasında, uzun ve özel hazırlanmış bir girdiyle CPU'yu saniyelerce meşgul eden bir regular expression buluyor: klasik bir ReDoS (Regular expression Denial of Service; düzenli ifadenin aşırı geri izleme yapmasıyla servisin yavaşlaması).

Tahmin et: araştırmacı bunu kime söyleyecek? Bookstore'un sitesinde bir güvenlik iletişim adresi yok, repoda SECURITY.md yok. Araştırmacı iyi niyetli ve bir yol arıyor; bulduğu tek yol public bir GitHub issue'su. Issue açıldığı anda zafiyet, düzeltmesi olmadan herkesin önünde. Bu yazıda bu senaryonun nasıl olmaması gerektiğini konuşuyoruz: bildirimi nasıl karşılarsın, nasıl triage edersin, düzeltmeyi gizli tutup advisory ve CVE (yayımlanmış güvenlik açıklarına verilen standart kimlik numarası) ile nasıl duyurursun.

security.txt: Kapıdaki İletişim Bilgisi

Bir sitenin güvenlik iletişim bilgisini bulmanın standart yeri /.well-known/security.txt dosyası. Bu dosyayı RFC (Request for Comments; internet standartlarının yayımlandığı doküman serisi) 9116 tanımlıyor; Nisan 2022'de E. Foudil ve Y. Shafranovich tarafından yayımlandı. Kurallar kısa: dosya HTTPS üzerinden, text/plain; charset=utf-8 olarak sunulur. Contact zorunludur ve birden fazla olabilir; Expires zorunludur, tek bir kez yazılır ve bir yıldan yakın bir tarih olması önerilir. Preferred-Languages, Canonical, Policy, Acknowledgments, Encryption ve Hiring isteğe bağlı. IANA (internet protokol kayıtlarını tutan kuruluş) alan listesine sonradan iki alan daha eklendi: 2023'te CSAF, Mart 2026'da Bug-Bounty. Dosyanın OpenPGP ile imzalanması da öneriliyor.

Bookstore'un dosyası repoda, kodun yanında duruyor ve her değişikliği PR (pull request; kod değişikliği isteği) ile gözden geçiriliyor:

Contact: mailto:security@bookstore.example.com
Contact: https://bookstore.example.com/security/report
Expires: 2027-05-15T00:00:00Z
Preferred-Languages: tr, en
Canonical: https://bookstore.example.com/.well-known/security.txt
Policy: https://bookstore.example.com/security/policy
Acknowledgments: https://bookstore.example.com/security/thanks

API dosyayı başlangıçta okuyup olduğu gibi sunuyor. Dosyayı dinamik üretip Expires'ı her istekte bir yıl ileri atmak cazip gelebilir; ama alanın amacı, bilginin güncelliğinin bir insan tarafından onaylandığını göstermek. Kendini kendiliğinden yenileyen bir tarih bu sinyali öldürür.

namespace Bookstore.Security;

// RFC 9116: dosya repoda durur, kod gibi review edilir ve olduğu gibi sunulur.
public static class SecurityTxtEndpoint
{
    public const string Path = "/.well-known/security.txt";

    public static IEndpointRouteBuilder MapSecurityTxt(this IEndpointRouteBuilder app)
    {
        IWebHostEnvironment env = app.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
        string content = File.ReadAllText(System.IO.Path.Combine(env.ContentRootPath, "security.txt"));

        app.MapGet(Path, () => Results.Text(content, "text/plain; charset=utf-8"))
            .ExcludeFromDescription();
        return app;
    }
}

Dosyanın publish çıktısına girmesi için csproj'a <None Update="security.txt" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> satırı eklendi. API'yi 127.0.0.1'de çalıştırıp isteği attığımızda:

curl -si http://127.0.0.1:52864/.well-known/security.txt
HTTP/1.1 200 OK
Content-Length: 343
Content-Type: text/plain; charset=utf-8
Date: <date>
Server: Kestrel

Contact: mailto:security@bookstore.example.com
Contact: https://bookstore.example.com/security/report
Expires: 2027-05-15T00:00:00Z
Preferred-Languages: tr, en
Canonical: https://bookstore.example.com/.well-known/security.txt
Policy: https://bookstore.example.com/security/policy
Acknowledgments: https://bookstore.example.com/security/thanks

Yerelde HTTP kullandık; production'da TLS (Transport Layer Security; trafiği şifreleyen protokol) kenarda sonlanıyor ve dosya yalnız HTTPS'ten erişilebilir. Biçimi, Hollanda'daki Digital Trust Center'ın geliştirdiği açık kaynak doğrulayıcı sectxt ile kontrol ettik; küçük script repoda tools/check-security-txt.py olarak duruyor:

pip install sectxt==0.9.8
python tools/check-security-txt.py src/Bookstore.Api/security.txt
valid: True
recommendation: no_encryption: 'Encryption' field should be present when 'Contact' field contains an email address.
recommendation: not_signed: security.txt should be digitally signed.

İki öneri de bilinçli kararlara karşılık geliyor. Bookstore ekibinin yönettiği bir OpenPGP anahtarı yok; anahtarı olmayan bir ekibin şifreli e-posta beklemesi, raporu kimsenin açamadığı bir kutuya düşürür. Bunun yerine ikinci Contact satırı HTTPS üzerinden çalışan bir form. İmza da aynı nedenle şimdilik yok; ekip anahtar yönetimini kurduğunda ikisi birlikte eklenecek. Bu karar threat-model/risk-register.md dosyasına bir satır olarak girdi.

Expires unutulan bir alandır. Bookstore'da bir test, tarihin bir yıldan uzak olmadığını ve 30 günden yakın olmadığını kontrol ediyor; son 30 günde build kırmızıya dönüp yenilemeyi hatırlatıyor:

[Fact]
public async Task Security_txt_expires_in_less_than_a_year_and_not_within_30_days()
{
    string body = await factory.CreateClient().GetStringAsync("/.well-known/security.txt", ct);
    string value = body.Split('\n').Single(l => l.StartsWith("Expires: ", StringComparison.Ordinal))["Expires: ".Length..];
    DateTimeOffset expires = DateTimeOffset.Parse(value, CultureInfo.InvariantCulture);
    TimeSpan left = expires - DateTimeOffset.UtcNow;

    // RFC 9116 bir yıldan kısa öneriyor; bitişe 30 gün kala test kırılır ve yenilemeyi hatırlatır.
    Assert.True(left < TimeSpan.FromDays(365), $"Expires is too far away: {value}");
    Assert.True(left > TimeSpan.FromDays(30), $"security.txt expires on {value}: renew it and ask the security champion to review the contacts.");
}

SECURITY.md ve GitHub Private Vulnerability Reporting

security.txt siteye gelen araştırmacı içindir; repoya gelen araştırmacı ise önce SECURITY.md dosyasına bakar. GitHub bu dosyayı repo kökünde, docs/ ya da .github/ klasöründe tanır ve Security sekmesinde gösterir. Bookstore'un dosyası dört soruya cevap veriyor: nereye bildirilir, sonra ne olur, hangi sürümler destekleniyor ve iyi niyetli araştırmacıya ne söz veriliyor. "Sonra ne olur" kısmı:

| Step | Target |
|---|---|
| We confirm that we received the report | 3 business days |
| First assessment: valid or not, severity, affected versions | 10 business days |
| Fix released: Critical / High | 14 days / 30 days after the assessment |
| Fix released: Medium / Low | Next planned release, at most 90 days |
| Public advisory | When the fix is available, on a date agreed with you |

Hedef süreleri yazmak ekibi bağlar; bu iyi bir şey. Araştırmacı ne zaman haber alacağını bilirse sabırlı olur, bilmezse sessizliği ilgisizlik sayar ve kendi tarihini kendisi belirler.

GitHub tarafında ikinci adım private vulnerability reporting. Açık kaynak repolarda repo sahibi ya da admin bu özelliği repo ayarlarındaki güvenlik bölümünden açar; sonra herkes Security sekmesindeki "Report a vulnerability" butonuyla özel bir rapor gönderebilir. Rapor, bakımcıların ve raporu yapanın görebildiği bir draft security advisory olarak açılır; raporu yapan, advisory'ye otomatik olarak collaborator ve credit alan kişi olarak eklenir. Dikkat: SECURITY.md eklemek bu özelliği açmaz; ikisi ayrı ayarlar.

Triage, Draft Advisory ve CVE

Rapor geldiğinde ilk cevap üç iş günü içinde gider ve kısadır: teşekkür, kimin takip ettiği, bir sonraki haberin ne zaman geleceği. İlk cevapta "bu bir zafiyet değil" tartışmasına girilmez; değerlendirme yapılmadan verilen ret, araştırmacıyı public yola iter. Triage'da zafiyet yerelde ya da test ortamında yeniden üretilir, gerçekten bir zafiyet olup olmadığına karar verilir, severity belirlenir ve etkilenen sürümler listelenir. Bulunmuş bir zafiyetin puanı için CVSS (Common Vulnerability Scoring System; bilinen zafiyetler için puanlama sistemi) kullanılır, güncel sürümü FIRST'in (Forum of Incident Response and Security Teams; olay müdahale ekiplerinin uluslararası forumu) Kasım 2023'te yayımladığı 4.0; puanlamanın ayrıntısı DevSecOps serisinin dependency scanning yazısında.

Düzeltme draft advisory içinde yürür. GitHub dokümantasyonuna göre draft advisory yalnız bakımcılara ve eklenen collaborator'lara görünür; advisory'den açılan geçici private fork'ta düzeltme public repoya dokunmadan hazırlanıp review edilir. Aynı ekrandan CVE de istenir: GitHub bir CNA (CVE Numbering Authority; belirli bir kapsam için CVE kimliği vermeye yetkili kuruluş), yani advisory'yi yayımlamadan kimlik talep edebilirsin. Dokümana göre GitHub bu talebi genelde 72 saat içinde inceliyor. CVE programında bir kimlik önce Reserved durumundadır, kayıt bilgileri doldurulup yayımlanınca Published olur; kullanılmaması gereken bir kimlik ise Rejected olarak listede kalır. cve.org'un Mart 2026 tarihli duyurusuna göre program içinde 500'ü aşkın CNA var.

Advisory'nin metni bir şablondan yazılır; Bookstore'da şablon repoda, bir runbook'un parçası olarak duruyor:

# Security advisory template (GitHub repository security advisory)

Fill this in the draft advisory, never in a public issue or a commit message.

- Title: <component>: <weakness> allows <impact>
- Package / ecosystem: NuGet, `Bookstore.Contracts`
- Affected versions: `>= 2.0.0, < 2.1.1`
- Patched versions: `2.1.1`
- Severity: CVSS 4.0 vector and score, plus our own reasoning in one sentence
- Weakness: CWE id
- Credits: reporter name or handle, as they want to be credited

Bu serinin hazırlığında gerçek bir advisory, repo ya da CVE talebi oluşturmadık; buradaki adımlar GitHub'ın güncel dokümantasyonuna dayanıyor.

Yayın Günü: Bookstore.Contracts 2.1.1

Koordineli yayın, düzeltmenin ve duyurunun aynı gün, araştırmacıyla anlaşılan tarihte çıkması demek. Sıra önemli: önce düzeltilmiş paket, sonra advisory. Tersini yaparsan kullanıcılar zafiyeti öğrenir ama yükseltecek bir sürüm bulamaz; saldırgan ise aynı bilgiyle onlardan önce davranır.

04-disclosure-timeline-tr.png

Bookstore için akış şöyle: 2.1.1, Versioning ve Release Management serisinin NuGet paketi yayımlama yazısındaki pipeline ile nuget.org'a çıkar. Paket indekslenince draft advisory publish edilir. GitHub, NuGet gibi desteklediği ekosistemlerde yayımlanan advisory'yi GitHub Advisory Database'e alır ve bu kayıt, etkilenen sürümü kullanan repolarda Dependabot alert'lerini tetikler. Bookstore'un kendi API'si için ayrıca release notes'a bir satır ve kurumsal müşterilerin entegrasyon ekiplerine kısa bir e-posta gider: hangi sürüm etkileniyor, ne yapmaları gerekiyor, advisory'nin linki.

En sık yapılan hata zaman çizelgesinin dördüncü adımında olur. Düzeltme commit'i "Fix ReDoS in IsbnValidator" mesajıyla public main'e girerse, advisory yayımlanmadan önce zafiyetin yeri ve türü herkese açıklanmış olur; commit geçmişini izleyen biri için bu yeterli bir ipucu. Bookstore'un runbook'u bu yüzden düzeltmeyi private fork'ta tutuyor ve commit mesajlarının nötr olmasını istiyor.

Bug Bounty Ne Zaman Mantıklı?

Bug bounty, geçerli bulgulara para ödenen bir programdır; VDP (Vulnerability Disclosure Policy; zafiyet bildirim politikası) ise ödeme olmadan bildirimi nasıl kabul ettiğini anlatan politikadır. OWASP'ın (Open Worldwide Application Security Project; açık uygulama güvenliği topluluğu) Vulnerability Disclosure Cheat Sheet'i bug bounty'nin faydalı ama pahalı olduğunu anlatıyor: düşük kaliteli raporları ayıklamak zaman alır, production'a karşı sürekli test yapılır, ödemeler bütçe ister. Bu yüzden önerisi, bug bounty'yi ancak olgun bir bildirim sürecine sahip ekiplerin düşünmesi.

Bookstore için karar net: önce VDP. Raporları üç iş gününde cevaplayamayan bir ekip, para teklif ederek daha fazla rapor çekerse sorunu büyütür. Bug bounty'yi tartışmanın zamanı, VDP bir yıl boyunca hedeflerini tuttuktan sonra.

VDP'nin en önemli parçası safe harbor metnidir: politikaya uyan iyi niyetli araştırmacıya karşı hukuki işlem başlatılmayacağı sözü. Bookstore'un SECURITY.md'sindeki metin kısa ve kendi cümlelerimizle yazıldı: yalnız kendi hesapların ya da adı verilen test ortamı, başkasının verisine gerekenden fazla dokunmamak, servisi yavaşlatmamak, yayımlamadan önce makul süre tanımak. Daha kapsamlı bir metin gerekiyorsa disclose.io'nun CC0 (telif hakkından feragat eden açık lisans) ile yayımlanan şablonları iyi bir başlangıç; yine de son metni bir hukukçuya okut.

Üretimde Dikkat

security@ adresi bir kişinin değil, bir rotasyonun adresidir. Tatildeki tek kişiye giden rapor, üç iş günü hedefini sessizce kaçırır. Bookstore'da adres security champion ile tech lead'e birlikte gidiyor ve gelen her rapor için otomatik bir backlog item açılıyor.

Canonical alanı, dosyanın gerçekten sunulduğu adresle aynı olmalı; aksi hâlde doğrulayıcılar dosyayı başka bir yerden kopyalanmış sayar. Birden fazla alan adın varsa her birinde dosya bulunmalı ya da hepsi aynı adrese yönlenmeli.

Araştırmacıyı bilgilendirmeye devam et. SECURITY.md en az iki haftada bir haber vermeyi söz veriyor; düzeltme gecikiyorsa bunu açıkça söylemek, sessiz kalmaktan her zaman daha iyidir. Advisory'de araştırmacının nasıl anılmak istediğini sor ve öyle yaz.

Sık Düşülen Tuzaklar

Bildirimi yapanı görmezden gelmek, en pahalı hatadır. Cevapsız kalan araştırmacı çoğu zaman bulgusunu kendi tarihinde yayımlar ve o tarih senin düzeltmenle çakışmayabilir. Daha kötüsü, hukuki tehditle cevap vermek: bu hem araştırmacı topluluğunda kalıcı bir itibar kaybıdır hem de bir sonraki bulgunun sana değil doğrudan internete gitmesini sağlar.

İkinci tuzak düzeltmeyi advisory'den önce açıklayıcı bir commit mesajıyla yayımlamak; yukarıda anlattık. Üçüncüsü advisory'yi düzeltilmiş sürümden önce publish etmek. Dördüncüsü Expires tarihi geçmiş bir security.txt: süresi dolmuş bir iletişim bilgisi, araştırmacıya "burada kimse yok" der.

Küçük Bir Deneme

★☆☆ security.txt'teki Expires tarihini bugünden 20 gün sonraya çek ve testleri çalıştır. Hata mesajı champion'a ne yapması gerektiğini söylüyor mu?

★★☆ SECURITY.md'deki hedef sürelerin her biri için bir ölçüm tanımla: hangi tarih nereden okunur, kim raporlar? Üç ay sonra bu ölçümler neyi gösterirse süreç başarılı sayılır?

★★★ Bookstore.Contracts için bir ISBN doğrulama fonksiyonu yaz ve bir ReDoS zafiyetine karşı nasıl korunduğunu gösteren bir test ekle (ipucu: RegexOptions.NonBacktracking ya da bir matchTimeout). Sonra advisory şablonunu bu senaryo için doldur.

Dışarıdan gelen bildirimi karşılamayı konuştuk. Bazen ise dışarıdan birini kendimiz çağırırız: kurumsal bir müşteri bir pentest raporu ister ya da büyük bir release öncesi bağımsız bir göz isteriz. Serinin beşinci yazısında pentest'in kapsamını, rules of engagement'ı, bulguları okumayı ve retest'i konuşuyoruz.