İçindekiler
- 1 Giriş: Bir Script, Bir Kahve ve Küçük Bir Panik
- 2 CSP Neyi Çözer, Neyi Bozar? Zihinde Doğru Çerçeveyi Kurmak
- 3 Nonce mi Hash mi? Inline Script’lerle Uyumlu Yaşamanın Yolları
- 4 WordPress’te CSP: Enqueue Dünyasında Nonce, Hash ve Ufak Tuzaklar
- 5 Laravel’de CSP: Middleware, Blade ve Vite/Livewire Uyumunu Kurmak
- 6 Raporlama: report-to, report-uri ve İhlalleri Sakin Sakin İzlemek
- 7 Sunucu Başlıkları, CDN ve Dağıtım Rutinleri: İş Sahada Bitiyor
- 8 Küçük Pratikler: Inline’ı Azalt, Davranışı Taşı, İhlali İzle
- 9 Örnek Politikalar: Başlangıç İçin Tatlı Bir Taban
- 10 Kapanış: CSP’yi Gündelik Hayata Katmak
Giriş: Bir Script, Bir Kahve ve Küçük Bir Panik
Hiç şöyle oldu mu? Siteyi canlıya atmışsın, her şey tıkır tıkır. Sonra bir gün, müşteri “sepette butonlar çalışmıyor” diye arıyor. Tarayıcı konsoluna bakıyorsun: “Refused to execute inline script because it violates the Content Security Policy”. O an bir kahve alıp derin nefes çekiyorsun. Evet, konu CSP. İyi kurulduğunda harika bir kalkan; yanlış kurulduğunda, sitede masum görünen her tıklamayı bile durdurabilen bir bekçi.
Benim de başıma geldi. Bir WordPress teması, iki minik inline script ve bir Laravel projesinde Vite ile canlı güncelleme derken, CSP hepsini duvara toslattı. Sonra düşündüm ki: “Bu işi sakin sakin toparlayalım. Nonce en mantıklısı mı, hash nerede işimize yarar, report-to nasıl devreye girer, inline script’leri nasıl ehlileştiririz?” Bu yazıda, aynen bir arkadaş muhabbeti gibi, baştan sona birlikte yürüyelim. WordPress ve Laravel üzerinde örneklerle, nonce/hash seçiminden report-to’ya, inline script’leri güvenli hâle getirme yollarına kadar pratik bir rehber olacak.
Hazırsan kahveni kap, konsolu aç, birlikte CSP’yi bir yük değil, bir konfor alanına dönüştürelim.
CSP Neyi Çözer, Neyi Bozar? Zihinde Doğru Çerçeveyi Kurmak
CSP aslında “kaynakları kimden ve nasıl yükleyeceğimizi” tarif eden bir güvenlik politikası. Bir nevi siteye güvenlikten anlayan bir bekçi koymak gibi. Bu bekçi bilmediği bir kaynaktan script görünce içeri almıyor. Bu güzel; çünkü kötü niyetli eklenen bir snippet’in etkisini kırıyor. Ama bir de madalyonun öbür yüzü var: Tema kurarken, minik bir onclick yazmışsın ya da bir eklenti ufak bir inline script basmış; bekçi bunu da sevmiyor. İşte can sıkan yer burası.
Ben bu işe hep şöyle yaklaşıyorum: Önce en sıkı ayarla başlıyorum; sonra “gerçekten” ihtiyacım olan kapıları tek tek açıyorum. “default-src ‘self’” gibi dar başlayan bir politika, bana nerede takıldığımı çok net gösteriyor. Ardından script ve style için gerekiyorsa nonce, değişmeyecek minik kodlar için hash, üçüncü parti servisler için spesifik connect-src, img-src gibi direktiflerle daireyi genişletiyorum. Böylece kontrol bende kalıyor, panik yok.
Şunu da söyleyeyim: “unsafe-inline” ile işi bir gecede çözüp geçmek kolay. Ama bu, kapıyı ardına kadar açmak demek; CSP’den beklediğimiz güvenlik etkisini büyük ölçüde düşürüyor. Onun yerine “inline” ihtiyaçları usul usul nonce veya hash ile ehlileştirmek, orta vadede çok daha huzurlu bir hayat sunuyor.
Nonce mi Hash mi? Inline Script’lerle Uyumlu Yaşamanın Yolları
Nonce ve hash, inline script’leri “akredite” etmenin iki tatlı yolu. Nonce, sayfa yüklenirken rastgele üretilen bir bilet gibi düşünülür. Senin script etiketinin üstüne bu bileti (nonce değeri) iliştirirsin; CSP de “tamam, bu bilet benden, geçiş serbest” der. Avantajı, içerik değişse bile aynı sayfa içindeki script nonce’ı taşıdığı sürece çalışır. Dezavantajı, her istekte yeni bir nonce üretip script etiketlerine takmayı unutmamalısın.
Hash ise içeriğin parmak izi. Inline kodun değişmezse harika çalışır. Mesela her sayfada aynı ufak “theme switcher” satırın var; bir kere hash’ini alırsın, CSP’ye eklersin, bitti. Buradaki düğüm, kodda minicik bir boşluk değişse bile hash’in değişmesidir. Otomasyonun yoksa uğraştırır; ama değişmeyen snippet’ler için temiz bir çözüm.
Ben genelde şöyle yapıyorum: Büyük picture’da tüm script’leri mümkün olduğunca dosyaya taşıyorum. Kalan mecburi inline’lar için nonce kullanıyorum. Bir iki yerde hiç değişmeyen minicik kodlar varsa, onlar için hash. Böylece ne “unsafe-inline”a mecbur kalıyorum, ne de projeyi boğacak kadar kural ekliyorum. Denge tatlıdır.
WordPress’te CSP: Enqueue Dünyasında Nonce, Hash ve Ufak Tuzaklar
Temel yaklaşım: Her istekte nonce üret, script/style etiketlerine iliştir
WordPress’te işin kalbi enqueue sistemi. Bir sayfada hangi script ve stilin yükleneceğini buradan yönetiyoruz. CSP için yaklaşım basit: Her istekte bir nonce üret, hem yanıt başlığına (header) koy, hem de WordPress’in bastığı script ve style etiketlerine iliştir. Böylece inline yazmak zorunda kaldığın ufak kodlar da yaşamaya devam edebilir.
// functions.php veya bir küçük mu-plugin
function dc_csp_get_nonce() {
static $nonce = null;
if ($nonce === null) {
$nonce = base64_encode(random_bytes(16));
}
return $nonce;
}
add_action('send_headers', function () {
$nonce = dc_csp_get_nonce();
$csp = "default-src 'self'; "
. "base-uri 'self'; object-src 'none'; frame-ancestors 'self'; "
. "img-src 'self' data: blob:; font-src 'self' data:; "
. "connect-src 'self'; "
. "style-src 'self' 'nonce-{$nonce}'; "
. "script-src 'self' 'nonce-{$nonce}'; "
. "report-uri https://rapor.example.com/csp";
header('Content-Security-Policy: ' . $csp);
});
add_filter('script_loader_tag', function ($tag) {
$nonce = dc_csp_get_nonce();
return str_replace('<script ', "<script nonce='{$nonce}' ", $tag);
}, 10);
add_filter('style_loader_tag', function ($tag) {
$nonce = dc_csp_get_nonce();
return str_replace('<link ', "<link nonce='{$nonce}' ", $tag);
}, 10);
Burada iki kritik nokta var. Birincisi, nonce her istek için taze olmalı. İkincisi, WordPress’in bastığı tüm script ve style etiketlerine bu nonce’ı takmayı unutmamak gerekiyor. “Ben inline yazacağım, nasıl olacak?” dersen, WordPress’in yeni fonksiyonu imdada yetişiyor:
// WP 5.7+: inline script basarken nonce verebilirsin
wp_print_inline_script_tag(
'console.log("Merhaba CSP");',
array( 'nonce' => dc_csp_get_nonce() )
);
Inline bağımlılıklar: onclick’ten addEventListener’a minik göç
WordPress temalarında sık gördüğüm şeylerden biri, HTML etiketlerinin üstünde onclick, onchange gibi inline event’ler. CSP bunu sevmez. Çözüm basit: O event’leri kaldır, ilgili elementi bir id veya data-attribute ile işaretle, açılışta bir addEventListener ile bağla. İlk başta zahmetli gibi görünür; ama düzenli bir yapıya geçince, tema zaten daha okunaklı oluyor.
<!-- Eskiden -->
<button id='buy' onclick='addToCart()'>Sepete Ekle</button>
<!-- Sonra -->
<button id='buy' data-action='add-to-cart'>Sepete Ekle</button>
<script nonce='...'
>document.querySelector('[data-action="add-to-cart"]').addEventListener('click', addToCart);</script>
Emoji, oEmbed ve ufak sürprizler
WordPress bazı sayfalara otomatik inline kodlar serpiştirebiliyor. Emoji algılama script’i, oEmbed önizlemelerinin bazı dokunuşları derken CSP “dur bakalım” diyebilir. İhtiyacın yoksa bu özellikleri kapatmak rahatlatır.
// Emoji script ve stilini kapat
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
Üçüncü parti servisler (analitik, harita, chat widget’ları) için de doğru script-src ve connect-src domain’lerini CSP’ye eklemek gerekir. Eğer etiketi dosya olarak yüklüyorsan, nonce taşımasına gerek yok. Ama beklenmedik bir inline snippet basıyorsa, onun için nonce veya hash düşünmelisin.
Hash ile değişmeyen küçük snippet’ler
Hiç değişmeyen küçücük bir inline kodun varsa, hash yaklaşımı pürüzsüz çalışır. Örneğin:
$snippet = "document.body.classList.add('ready');";
$sha256 = base64_encode(hash('sha256', $snippet, true));
$csp_part = "'sha256-{$sha256}'"; // script-src'ye ekle
Burada önemli olan, snippet’in birebir aynı olması. Boşluk bile değişirse hash uymayacak. Bunun için snippet’i bir dosyaya taşıyıp build sürecinde hash üretmek, sonra CSP’ye enjekte etmek temiz bir pratik.
Laravel’de CSP: Middleware, Blade ve Vite/Livewire Uyumunu Kurmak
Middleware ile merkezi politika, request başına nonce
Laravel’de en sevdiğim yaklaşım, bir middleware ile tek merkezden CSP üretmek. Request başına nonce üret, hem header’a koy hem de Blade’e taşı. Böylece layout içinde bastığın script’ler kolayca uyumlanır.
// app/Http/Middleware/ContentSecurityPolicy.php
namespace AppHttpMiddleware;
use Closure;
class ContentSecurityPolicy
{
public function handle($request, Closure $next)
{
$nonce = base64_encode(random_bytes(16));
app()->instance('csp_nonce', $nonce);
$response = $next($request);
$policy = "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; "
. "img-src 'self' data: blob:; font-src 'self' data:; "
. "connect-src 'self' https://api.example.com; "
. "style-src 'self' 'nonce-{$nonce}'; "
. "script-src 'self' 'nonce-{$nonce}'; "
. "report-uri https://rapor.example.com/csp; report-to csp";
// Reporting-Endpoints (yeni dünyaya bir kapı)
$response->headers->set('Reporting-Endpoints', "csp='https://rapor.example.com/reports'");
$response->headers->set('Content-Security-Policy', $policy);
return $response;
}
}
// app/Providers/AppServiceProvider.php
use IlluminateSupportFacadesVite;
public function boot()
{
if (function_exists('csp_nonce')) {
Vite::useCspNonce(csp_nonce());
}
}
// app/helpers.php (örnek yardımcı)
if (! function_exists('csp_nonce')) {
function csp_nonce(): string
{
return app('csp_nonce') ?? '';
}
}
Artık Blade’de nonce kullanmak kolay:
<script nonce='{{ csp_nonce() }}'>window.appReady = true;</script>
Vite, Livewire, Alpine ve arkadaşları
Vite kullandığında, Laravel’in Vite::useCspNonce() desteği işini ciddi kolaylaştırır. Ürettiğin nonce otomatik script etiketine taşınır. Livewire/Alpine tarafında, inline event’leri mümkün olduğunca terk edip, komponentlerin kendi yaşam döngüsünde addEventListener ile bağlanmak işleri pürüzsüz yapar. Eğer Livewire sürümünde nonce taşıma desteği zayıfsa, inline yerine defer yüklü harici bir JS dosyasıyla davranışı dışarı almayı düşün. Genelde bir iki düzenleme ile bütün komponentler CSP ile barışıyor.
Hash’ler nerede işimize yarar?
Laravel projesinde hiç değişmeyen bir iki minik inline kod varsa, build aşamasında hash üretip politika içine gömmek ferahlatıcıdır. Mesela bir Blade parçasında 20 karakterlik bir helper scriptin varsa, onu bir sabit gibi düşün, hash’le, CSP’ye ekle. Böylece hem “unsafe-inline”a bulaşmazsın, hem de deployment sırasında sürpriz yaşamazsın.
Raporlama: report-to, report-uri ve İhlalleri Sakin Sakin İzlemek
CSP’nin gizli kahramanı raporlama. Bir şey bloklandığında tarayıcı bir rapor yollar; sen de nerede takıldığını görürsün. Burada iki yol var: report-uri ve report-to. Birçok tarayıcı hâlâ report-uri’yi destekliyor, bazıları ise yeni report-to yaklaşımına kapı açıyor. Pratikte ikisini birden koymak, geçiş dönemlerinde huzur veriyor.
// Örnek header parçaları
Content-Security-Policy: ...; report-uri https://rapor.example.com/csp; report-to csp
Reporting-Endpoints: csp='https://rapor.example.com/reports'
Report endpoint işi için ister kendi ufak bir endpoint’ini yaz, ister bir hizmet kullan. Ben bazen test aşamasında hızlıca rapor toplama hizmetleri ile başlar, sonra üretimde kendi basit endpoint’ime geçerim. Böylece üçüncü partiye bağımlılık azalır. Raporların sık geleceğini unutma; log rotasyon, basit bir filtreleme ve “şu domainleri zaten biliyoruz” türü beyaz liste ufkunu genişletir.
CSP direktiflerinin detaylarına takıldığında, kısa ve net anlatımları için MDN’nin CSP sayfası gerçekten nefes aldırır. Ayrıca web.dev üzerinde CSP rehberi pratik notlarla iyi bir tamamlayıcı.
Sunucu Başlıkları, CDN ve Dağıtım Rutinleri: İş Sahada Bitiyor
Nginx/Apache/Proxy: Başlığı nerede set edeceğiz?
Benim rutinim şu: Geliştirme aşamasında framework ile CSP’yi set ederim (WordPress’te PHP, Laravel’de middleware). Üretimde ise önündeki Nginx/Apache katmanına da bir “minimum garanti” politikası koyarım. Örneğin maintenance sayfası veya framework’e uğramadan dönen statik bir hata sayfası bile en temel CSP’ye sahip olsun. Böylece olağanüstü durumlarda bile çıplak kalmam.
# Nginx örneği (temel politika)
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'" always;
CDN kullanıyorsan, header’ların cache’lenme davranışını unutma. Bazı CDN’ler yanıt başlıklarını farklı şekillerde normalize edebilir. Bir de “HTML sadece bu sunucudan gelir” diyip, JS/CSS’yi CDN’den çekiyorsan, script-src ve style-src için ilgili domainleri mutlaka ekleme listesinde tut. Güvenli ve sade.
Güvenlik katmanlarını yan yana dizmek
CSP, tek başına her derdin ilacı değil. Ben genelde onu, TLS’in doğru ayarlandığı, kaynak sunucunun kimliğinin gerçekten doğrulandığı bir dizinin parçası yapıyorum. Mesela kaynağı gerçekten korumak için mTLS ve origin doğrulaması gibi pratikler yanında CSP de oturdu mu, trafik hatlarını sağlamlaştırmış oluyorsun. Katmanlı güvenlik, küçük aksiliklerde bile sistemin ayakta kalmasını sağlıyor.
Küçük Pratikler: Inline’ı Azalt, Davranışı Taşı, İhlali İzle
Bir projeyi CSP’ye geçirmek, bazen tek bir akşamda yönetilebilir. Ben ufak bir yol haritası izliyorum. Önce sitede inline event’leri ayıklıyorum. Küçük script’leri dosyalara taşıyorum. CSS içinde inline stil patlamışsa, kritik olanları bir dosyaya çekiyorum. Bu adımlar tek başına politikayı sadeleştiriyor.
Sonra raporlama ile küçük yamaları yapıyorum. “Şu domain connect-src’ye eklenmeli, bu avatar URL’si img-src’de data: gerekiyor, şu font için font-src self + data: yeter” gibi. Üçüncü parti servisleri kontrollü eklemek, “bir kerelik” denilen snippet’lerin kalıcı davranmadığını da görmeye yarıyor. Geçici iş için geçici izin; kalıcı olan için kalıcı kural.
Bir de ekip iletişimi. CSP devredeyken herkes ufak bir farkındalığa alışıyor. Tasarımcı inline stil yazacağını bildiğinde, bir dosyaya koymayı artık refleks hâline getiriyor. Geliştirici onclick yerine event listener’ı tercih ediyor. İşte o zaman CSP görünmez bir yardımcı oluyor; görünür olduğunda genelde bir şey bozulur çünkü.
Örnek Politikalar: Başlangıç İçin Tatlı Bir Taban
Başlangıçta iş görecek, sonra projeye göre kıvırabileceğin bir politika şöyle durabilir. Bunu birebir alıp kullanmak yerine, kendi domain’lerini ve ihtiyaçlarını işleyerek ilerle:
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'self';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self';
style-src 'self' 'nonce-...';
script-src 'self' 'nonce-...' 'strict-dynamic';
report-uri https://rapor.example.com/csp;
report-to csp
Reporting-Endpoints:
csp='https://rapor.example.com/reports'
Buradaki strict-dynamic, nonce’lı bir script’in dinamik olarak yüklediği diğer script’lere de güven dairesini açar. Her zaman gerekli değil, ama bazı SPA düzenlerinde işleri azaltır. Yine de, “gereksiz yere kapı açtım mı?” diye bir dönüp bakmakta fayda var.
Kapanış: CSP’yi Gündelik Hayata Katmak
Özetle, CSP ilk bakışta ürkütebilir. Ama sakin bir planla yaklaşınca, günün sonunda kontrolün sende olduğu, sürprizlerin azaldığı bir düzen kuruyorsun. WordPress tarafında enqueue aklını ve wp_print_inline_script_tag’i kullanıp request başına nonce üretmek, en sık karşılaştığım dertlerin çoğunu çözüyor. Laravel’de middleware ile merkezi bir politika, Blade’de nonce, Vite için Vite::useCspNonce gibi mekanizmalarla iş rayına oturuyor.
Pratik tavsiyem şu: Önce en sıkı mümkün tabloyla başla, raporlamayı aç, sonra ihtiyaca göre gevşet. Inline’ları olabildiğince dosyalara taşı, mecburi kalanlar için nonce veya hash kullan. Üçüncü parti servisleri tek tek düşün; hepsi gerçekten gerekli mi? Raporları izlerken sabırlı ol; ilk günler biraz gürültü olur, sonra sessizlik güzelleşir.
Umarım bu yazı elini rahatlattı. Bir dahaki deploy’da konsolda kırmızı uyarı yerine sakin bir “All good” görmek isterim. Aklına takılan bir durum olursa, bir sonraki yazıda ya da mesaj kutusunda görüşürüz. Güvenliği katmanlı kurarken, CSP’yi doğru yerleştirmek müthiş bir fark yaratıyor. Bu arada güvenlik katmanlarını derinleştirmek istersen, origin doğrulaması ile kaynağı gerçekten korumaya da göz atabilirsin. Güzel bir tamamlayıcı.
