CSP Level 3, Trusted Types ve pratik kurulum
Bir XSS açığı bulunduğunda genellikle ilk soru şu olur: "CSP'niz var mıydı?" Çoğu projede ya yoktur ya da unsafe-inline ile etkisiz hale getirilmiştir. 2026 itibarıyla CSP Level 3 spesifikasyonu tüm büyük tarayıcılarda destekleniyor, Trusted Types API'si de Chromium tabanlı tarayıcılarda stable. Bu ikisini birlikte kurduğunuzda, inline script enjeksiyonlarını ve DOM XSS'i büyük ölçüde engelleyebilirsiniz.
CSP Level 3'te ne değişti
CSP Level 2'de script-src direktifine nonce veya hash eklediğinizde, bu değerler yalnızca <script> etiketlerini kapsıyordu. Level 3 ile gelen strict-dynamic direktifi oyunu değiştirdi. strict-dynamic aktifken, nonce'a sahip bir script'in dinamik olarak oluşturduğu alt script'ler de otomatik olarak güvenilir kabul ediliyor. Bu, https: veya http: gibi host-based allowlist'lerin göz ardı edilmesi anlamına geliyor.
Pratik etkisi: artık CDN domain'lerini tek tek script-src'ye eklemenize gerek yok. Tek bir nonce, zincir boyunca yeterli.
Level 3 ile gelen bir diğer ekleme: script-src-elem ve script-src-attr direktifleri. Bunlar inline event handler'ları (onclick, onload gibi) ve <script> etiketlerini ayrı ayrı kontrol etmenize izin veriyor. Bir framework kullanıyorsanız ve inline event handler kullanmıyorsanız, script-src-attr 'none' diyerek bu yüzeyi tamamen kapatabilirsiniz.
Nonce mi, hash mi
İkisi de inline script'leri unsafe-inline olmadan çalıştırmanın yolları. Ama kullanım senaryoları farklı.
Nonce, her HTTP yanıtında sunucu tarafında üretilen rastgele bir değer. Script etiketine nonce attribute'u olarak eklenir, aynı değer Content-Security-Policy header'ında da yer alır. Eşleşme varsa script çalışır.
<!-- Sunucu her istekte yeni bir nonce üretir -->
<script nonce="dG9wc2VjcmV0MTIz">console.log('güvenli');</script>Content-Security-Policy: script-src 'nonce-dG9wc2VjcmV0MTIz' 'strict-dynamic'Hash ise script içeriğinin SHA-256 (veya SHA-384, SHA-512) özeti. İçerik değişmediği sürece hash sabit kalır, sunucu tarafında her istekte yeniden üretmeye gerek yoktur.
Content-Security-Policy: script-src 'sha256-abc123...='Nonce'ı tercih edin eğer:
- Server-side rendering yapıyorsanız (her yanıtta header'ı kontrol edebiliyorsunuz)
- Script içeriği dinamik olarak değişiyorsa
strict-dynamickullanmak istiyorsanız (hash ilestrict-dynamicda çalışır ama nonce daha yaygın)
Hash'i tercih edin eğer:
- Statik site üretiyorsanız ve HTTP header'ını kontrol edemiyorsanız (örneğin
<meta>etiketi ile CSP veriyorsanız) - Script içeriğinin asla değişmeyeceğini biliyorsanız
- CDN cache'leme nedeniyle her isteğe farklı nonce eklemek maliyetliyse
Bir uyarı: <meta http-equiv="Content-Security-Policy"> etiketi ile CSP verdiğinizde report-uri ve report-to direktifleri çalışmaz. Raporlama istiyorsanız HTTP header'ını kullanmak zorundasınız.
Next.js'te nonce kurulumu
Next.js 13.4+ sürümlerinde middleware ile nonce üretmek mümkün. Next.js resmi dokümantasyonu bu yaklaşımı öneriyor.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'nonce-${nonce}' 'strict-dynamic';
script-src-attr 'none';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\n/g, '');
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set('Content-Security-Policy', cspHeader);
return response;
}Ardından layout'ta nonce'ı headers() üzerinden okuyup <Script> bileşenlerine aktarabilirsiniz:
// app/layout.tsx
import { headers } from 'next/headers';
import Script from 'next/script';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headersList = await headers();
const nonce = headersList.get('x-nonce') ?? '';
return (
<html lang="tr">
<body>
{children}
<Script
src="https://analytics.example.com/script.js"
nonce={nonce}
strategy="afterInteractive"
/>
</body>
</html>
);
}Next.js 14+ sürümlerinde next.config.js içindeki experimental.serverActions ve PPR gibi özelliklerle birlikte çalışırken, middleware'deki nonce'ın streaming sırasında tutarlı kalmasına dikkat edin. Her NextResponse için tek bir nonce üretilmeli.
Trusted Types ile DOM XSS'i engellemek
CSP, inline script enjeksiyonlarını önler. Ama bir script zaten çalışıyorsa ve innerHTML, document.write, eval gibi tehlikeli sink'lere kullanıcı girdisi aktarıyorsa, CSP tek başına yetersiz kalır. Trusted Types tam bu noktayı kapatıyor.
Trusted Types'ı etkinleştirmek için CSP header'ınıza şu direktifi eklersiniz:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicyBu aktifken, innerHTML'e düz string atamaya çalıştığınızda tarayıcı TypeError fırlatır. Önce bir policy üzerinden string'i TrustedHTML'e çevirmeniz gerekir:
// Policy tanımlama
const sanitizePolicy = trustedTypes.createPolicy('myPolicy', {
createHTML: (input) => {
// DOMPurify gibi bir kütüphane ile sanitize edin
return DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true });
},
createScriptURL: (input) => {
const url = new URL(input, location.origin);
if (url.origin === location.origin) {
return url.toString();
}
throw new TypeError('İzin verilmeyen script URL: ' + input);
},
});
// Kullanım
const userContent = '<img src=x onerror=alert(1)>';
element.innerHTML = sanitizePolicy.createHTML(userContent);
// DOMPurify onerror handler'ı temizlerBurada trustedTypes.createPolicy ile oluşturduğunuz policy adının, CSP header'ındaki trusted-types direktifinde listelenmiş olması gerekiyor. Aksi halde policy oluşturma da engellenir.
React ve Trusted Types
React, dangerouslySetInnerHTML dışında DOM'a doğrudan HTML yazmaz. React 19 itibarıyla dangerouslySetInnerHTML Trusted Types ile uyumlu çalışıyor, yani bir TrustedHTML nesnesi aktarabilirsiniz. Ancak React'in kendi iç mekanizmaları (özellikle SSR hydration sırasında) bazı textContent atamaları yapabilir. Bu nedenle Trusted Types'ı report-only modda başlatıp ihlalleri gözlemlemek iyi bir strateji.
Firefox, Trusted Types'ı Nisan 2025 itibarıyla henüz varsayılan olarak desteklemiyor (flag arkasında, dom.security.trusted_types.enabled). Safari'de de destek yok. Bu nedenle Trusted Types'ı bir "katmanlı savunma" olarak düşünün: Chromium kullanıcılarınız için ek bir koruma katmanı.
Raporlama: report-to ve Reporting API
CSP ihlallerini bilmeden düzeltemezsiniz. Eski report-uri direktifi hâlâ çalışıyor ama deprecated. Yeni yol report-to direktifi ve Reporting-Endpoints header'ı.
Reporting-Endpoints: csp-endpoint="https://reports.example.com/csp"
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'; report-to csp-endpointÖnce Content-Security-Policy-Report-Only header'ı ile başlayın. Bu, ihlalleri raporlar ama hiçbir şeyi engellemez. Production trafiğiyle gerçek ihlal verisi topladıktan sonra Report-Only'yi kaldırıp enforcement moduna geçin.
Content-Security-Policy-Report-Only: script-src 'nonce-abc123' 'strict-dynamic'; report-to csp-endpointRaporlama endpoint'inize gelen JSON şu yapıda olur:
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src-elem",
"blocked-uri": "inline",
"source-file": "https://example.com/page",
"line-number": 42
}
}Bu raporları analiz etmek için Sentry gibi araçları kullanabilir ya da basit bir Hono.js endpoint'i yazabilirsiniz:
import { Hono } from 'hono';
const app = new Hono();
app.post('/csp-report', async (c) => {
const report = await c.req.json();
console.log('CSP ihlali:', JSON.stringify(report));
// Burada veritabanına kaydedin veya alerting sistemine gönderin
return c.json({ received: true });
});
export default app;Yaygın tuzaklar
Google Tag Manager gibi araçlar, dinamik olarak script enjekte eder. strict-dynamic kullanıyorsanız ve GTM script'ine nonce verdiyseniz, GTM'in oluşturduğu alt script'ler otomatik olarak çalışır. Ama GTM içinden eval() çağıran özel etiketleriniz varsa, bunlar engellenir. GTM custom template'lerinizi eval kullanmayacak şekilde güncellemeniz gerekir.
Üçüncü parti widget'lar (canlı sohbet, A/B test araçları) genellikle kendi script'lerini document.createElement('script') ile ekler. strict-dynamic aktifken, bu script'ler yalnızca nonce'lı bir parent script tarafından oluşturulmuşsa çalışır. Widget'ın loader script'ine nonce vermek yeterli olmalı, ama bazı widget'lar document.write kullanır. Bunlar Trusted Types aktifken de kırılır. Report-only modda test etmeden production'a almayın.
Bir diğer tuzak: style-src direktifinde unsafe-inline olmadan CSS-in-JS kütüphaneleri çalışmaz. styled-components v6+ nonce desteği sunuyor. Emotion da nonce prop'u kabul ediyor. Ama style attribute'u üzerinden stil yazan kütüphaneler (bazı animasyon kütüphaneleri gibi) style-src-attr direktifine takılabilir.
Minimum güvenli CSP şablonu
Aşağıdaki şablon, çoğu SSR uygulaması için iyi bir başlangıç noktası:
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
script-src-attr 'none';
style-src 'self' 'nonce-{RANDOM}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
require-trusted-types-for 'script';
trusted-types myPolicy;
report-to csp-endpoint{RANDOM} yerine her istekte sunucu tarafında crypto.randomUUID() veya crypto.getRandomValues() ile üretilmiş bir değer koyun. Base64 encode edilmiş en az 128 bit rastgelelik yeterli.
object-src 'none' ve base-uri 'self' direktiflerini unutmayın. Bunlar sıklıkla atlanır ama Flash/Java eklentileri üzerinden (evet, hâlâ bazı kurumsal ortamlarda var) ve <base> etiketi manipülasyonu üzerinden yapılan saldırıları engeller.
CSP ve Trusted Types birlikte kullanıldığında, XSS saldırı yüzeyinin büyük kısmı kapanır. Ama bu "XSS imkansız" demek değil. Sunucu tarafı template injection, policy'nizdeki bir sanitizasyon hatası veya nonce sızıntısı (nonce'ı bir GET parametresine yansıtmak gibi) hâlâ risk oluşturur. CSP'yi, authentication katmanınız ve input validation'ınız üzerine eklenen son bir savunma hattı olarak konumlandırın. Report-only modda başlayın, raporları birkaç hafta izleyin, false positive'leri temizleyin, sonra enforcement'a geçin. Bu sıralama, production'da beklenmedik kırılmaları önlemenin tek güvenilir yolu.