Content Security Policy ile XSS savunması: raporla, sıkılaştır, zorunlu kıl
XSS açıklarına karşı ilk savunma hattı her zaman girdiyi doğru işlemek ve çıktıyı doğru kodlamaktır. Ama ne kadar dikkatli olursanız olun, bir gün bir yerde kaçan bir innerHTML, bir üçüncü taraf betik ya da bir bağımlılık güncellemesi bu hattı delebilir. Content Security Policy (CSP), tarayıcıya "bu sayfada hangi kaynaklardan gelen kod çalışabilir" diye söyleyen ikinci bir katmandır. Doğru kurulursa enjekte edilmiş bir betiğin çalışmasını engeller.
CSP'nin zor tarafı, mevcut bir uygulamaya kırmadan eklemektir. Bu yazıda politikayı önce raporlama modunda yayına alıp, ihlalleri okuyup, nonce tabanlı bir politikaya geçerek zorunlu hâle getirme sürecini anlatıyorum.
CSP ne yapar, ne yapmaz
CSP bir HTTP yanıt başlığıdır. Tarayıcı, başlıktaki yönergelere göre betik, stil, görsel, bağlantı ve çerçeve kaynaklarını denetler. İzin verilmeyen bir kaynak yüklenmez ya da çalışmaz.
Bilmeniz gereken sınır şu: CSP açığı kapatmaz, sömürülmesini zorlaştırır. Çıktı kodlaması hâlâ gereklidir. CSP, o savunma başarısız olduğunda devreye giren bir emniyet kemeri gibi düşünülmeli. Ayrıca CSP sunucu tarafı yetkilendirme, CSRF ya da SQL enjeksiyonu gibi sorunlara çözüm değildir.
İzin listesi yaklaşımı neden yetmez
İlk akla gelen yol, güvenilen alan adlarını listelemektir:
Content-Security-Policy: script-src 'self' https://cdn.ornek.com
Bu görünüşte makul ama pratikte kırılgandır. İzin verilen alan adında barındırılan herhangi bir betik, örneğin açık bir JSONP uç noktası ya da eski bir kütüphane, saldırgana çalıştırılabilir kod sağlayabilir. Liste büyüdükçe bakımı da zorlaşır. Bu yüzden güncel öneri, alan adı yerine her yanıt için üretilen bir nonce'a ya da betik özetine (hash) dayanmaktır.
Nonce tabanlı politika
Fikir basit: sunucu her istekte rastgele bir değer üretir, bunu hem başlığa hem de güvenilen script etiketlerine yazar. Tarayıcı yalnızca bu nonce'u taşıyan betikleri çalıştırır.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{RASTGELE}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
frame-ancestors 'none'
ASP.NET Core'da bunu bir middleware ile yapabilirsiniz:
app.Use(async (context, next) =>
{
var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
context.Items["csp-nonce"] = nonce;
context.Response.Headers["Content-Security-Policy"] =
$"default-src 'self'; " +
$"script-src 'nonce-{nonce}' 'strict-dynamic'; " +
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'";
await next();
});
Önemli nokta: nonce her istekte yeniden üretilmeli ve tahmin edilemez olmalı. Sabit bir değer ya da sayfa önbelleğe alınırsa aynı nonce'un tekrar kullanılması korumayı sıfırlar. Tam sayfa önbelleği kullanıyorsanız nonce yerine hash tabanlı yaklaşımı düşünün.
'strict-dynamic', nonce ile güvenilen bir betiğin yüklediği başka betiklere de güvenilmesini sağlar. Böylece paket yükleyicilerin dinamik olarak eklediği dosyalar için tek tek izin yazmak gerekmez.
Önce Report-Only: kırmadan öğrenmek
Politikayı doğrudan zorunlu kılarsanız, unuttuğunuz bir satır içi betik üretimde bir butonu çalışmaz hâle getirebilir. Bunun yerine Content-Security-Policy-Report-Only başlığıyla aynı politikayı yayınlayın. Tarayıcı ihlalleri engellemez, yalnızca raporlar.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'nonce-{RASTGELE}' 'strict-dynamic';
report-to csp-endpoint
Rapor uç noktası basit bir POST alıcısı olabilir. Raporları bir tabloya yazın ve şunlara bakın: hangi yönerge ihlal edildi, hangi sayfada, hangi kaynak. Birkaç gün ya da bir hafta veri topladıktan sonra tipik gürültüyü ayıklarsınız: tarayıcı eklentilerinden gelen ihlaller, eski bir sayfadaki satır içi olay işleyicileri, unutulmuş bir analitik betiği.
Raporlama uç noktasını hız sınırlamasıyla koruyun. Herkese açık ve kimlik doğrulamasız bir uç nokta olduğu için kötü niyetli biri onu sahte raporlarla doldurabilir.
Satır içi kodu temizlemek
Raporlar çoğunlukla aynı üç şeyi gösterir:
onclick="..."gibi satır içi olay işleyicileri. BunlarıaddEventListenerile bir betik dosyasına taşıyın.- Satır içi
<script>blokları. Gerçekten gerekiyorsa nonce ekleyin, değilse harici dosyaya alın. evalya danew Functionkullanan kütüphaneler. Bunlar'unsafe-eval'gerektirir; mümkünse kütüphaneyi değiştirin ya da yapılandırmasını gözden geçirin.
Stil tarafında satır içi style öznitelikleri daha az risklidir ama aynı mantıkla yönetilebilir. İlk aşamada betikleri sıkılaştırıp stil politikasını sonraya bırakmak gerçekçi bir önceliktir; betik çalıştırma, stil enjeksiyonundan çok daha ciddi bir risktir.
Zorunlu kılmaya geçiş
Rapor akışı sakinleştiğinde başlığı Content-Security-Policy olarak değiştirin. Geçişi kademeli yapın: önce küçük bir trafik yüzdesi ya da yalnızca iç kullanıcılar için, sonra herkes için. Özellik bayrağıyla yönetirseniz sorun çıktığında tek adımda geri dönebilirsiniz.
Zorunlu kılmadan sonra da Report-Only'yi tamamen bırakmayın. Yeni bir politika sürümünü (örneğin daha sıkı bir style-src) Report-Only ile denerken mevcut politikayı zorunlu tutmak için iki başlığı birlikte gönderebilirsiniz. Her ikisi de bağımsız çalışır.
Sık yapılan hatalar
'unsafe-inline'eklemek. Sorunu hızlıca çözer ama betikler için CSP'nin değerini büyük ölçüde ortadan kaldırır. Nonce de varsa modern tarayıcılar'unsafe-inline'değerini zaten yok sayar; yine de bunu güvenlik mekanizması olarak saymayın.- Nonce'u önbelleğe almak. Aynı HTML'in farklı kullanıcılara aynı nonce ile gitmesi, nonce'u tahmin edilebilir kılar.
object-srcvebase-uriyönergelerini unutmak. Bu ikisi eksik kalırsa, betik kısıtlamasına rağmen başka yoldan kod çalıştırmak mümkün olabilir.- Raporları hiç okumamak. Rapor akışı kurulup izlenmezse CSP bir süre sonra sessizce eskir.
- Tek politikayı her yere uygulamak. Yönetim paneli ile genel sayfa farklı ihtiyaçlara sahipse, yola göre farklı politika üretmek mantıklı olabilir.
Kapanış
CSP, bir kere yazılıp unutulan bir başlık değil, uygulamayla birlikte yaşayan bir sözleşmedir. Önce Report-Only ile gözlemleyin, satır içi kodu temizleyin, nonce ve strict-dynamic ile politikayı sıkılaştırın, sonra kademeli olarak zorunlu kılın. Çıktı kodlamasının yerini almaz; ama bir gün kaçan bir enjeksiyonun sonucunu "betik çalışmadı ve rapor düştü" noktasına indirebilir. Bu da bir olayı yönetilebilir bir uyarıya çevirir.