Trusted Types'ı Production'a Almak: Angular Policy'leri, Üçüncü Parti Kütüphaneler ve Baseline 2026
Trusted Types, DOM sink'lerine düz string yerine yalnız izinli bir policy'nin ürettiği typed object kabul ettirir; DomSanitizer.sanitize sonucunu innerHTML'e yazmak bile ihlaldir. Bu yazıda Angular'ın policy adlarını node_modules'tan doğruluyor, Report-Only ile envanter çıkarıyor, DOMPurify, loader ve üçüncü parti script ihlallerini çözüyor, default policy'nin tehlikesini gösteriyor ve Firefox 148 ile Baseline 2026 olan desteği özetliyoruz.
Sürüm notu: Bu yazıdaki örnekler Angular 22.2, Node.js 24, Chrome 156, DOMPurify 3.4 ve playwright-core 1.65 ile denenmiştir.
Önce Tahmin Et
Bookstore'un Angular uygulamasına şu header'ı ekliyorsun: Content-Security-Policy: require-trusted-types-for 'script'; trusted-types angular. Uygulamada iki satır var. Birincisi template'te <p [innerHTML]="review.text">. İkincisi serinin önceki yazısındaki "düzeltilmiş" kod: nativeElement.innerHTML = this.sanitizer.sanitize(SecurityContext.HTML, text). İkisi de sanitize edilmiş HTML (HyperText Markup Language) yazıyor. Hangisi çalışır?
Yalnız birincisi. Trusted Types, değerin içeriğine değil tipine bakar. Sink, DOM'a (Document Object Model) HTML ya da script olarak yazan bir API'dir (Application Programming Interface); ona düz string gelirse tarayıcı atamayı reddeder. DomSanitizer.sanitize temizlenmiş bir string döndürür; tarayıcı için o hâlâ string. Template binding'i ise Angular'ın angular adlı policy'sinden geçer ve sink'e typed bir object ulaşır. Bu yazıda Trusted Types'ı Bookstore'da production'a alıyoruz: Angular'ın policy adları, Report-Only ile envanter, üçüncü parti library ve script ihlallerini tek tek çözmek, default policy'nin tehlikesi, ng serve ve test ortamı, Şubat 2026'da Baseline olan tarayıcı desteği. Trusted Types'ın temel fikri ve ilk require-trusted-types-for denemesi Web Güvenliği (OWASP Top 10) serisinin "OWASP A05: Injection (2)" yazısında anlatıldı.
İki Direktif, Bir Policy
Trusted Types iki CSP (Content Security Policy) direktifiyle açılır. require-trusted-types-for 'script' tarayıcıya "DOM XSS (Cross-Site Scripting) sink'lerine düz string kabul etme" der: innerHTML, outerHTML, insertAdjacentHTML, document.write, DOMParser.parseFromString, script.src, script.text, iframe.srcdoc, eval ve string alan setTimeout bunların başında. Bu sink'ler artık üç typed object'ten birini ister: HTML için TrustedHTML, script metni için TrustedScript, script URL'i (Uniform Resource Locator) için TrustedScriptURL. Bu object'leri yalnız bir policy üretebilir. Lab'daki loader'dan iki satır (tamamı aşağıda):
const loader = trustedTypes.createPolicy('bookstore#loader', { createScriptURL: toScriptUrl });
tm.src = loader.createScriptURL('http://127.0.0.1:19424/analytics.js');
trusted-types direktifi ise hangi adlarla policy oluşturulabileceğini sınırlar. Listede olmayan bir adla createPolicy çağrısı bir ihlaldir. Böylece güvenlik incelemesi bütün kod tabanı yerine birkaç policy fonksiyonuna iner: değer sink'e ancak bu fonksiyonlardan birinden geçerek ulaşabilir.

Angular'ın Policy Adları
angular.dev'deki güvenlik rehberi beş ad sayıyor ve hangisinin neden gerektiğini yazıyor. Lab'daki Angular 22.2 node_modules'unda gerçekten hangilerinin oluşturulduğuna baktım:
grep -rhoE --include='*.mjs' --include='*.js' "createPolicy\('[^']+'" node_modules/@angular/ | sort | uniq -c
1 createPolicy('angular#auto-csp'
1 createPolicy('angular#unsafe-bypass'
1 createPolicy('angular#unsafe-jit'
1 createPolicy('angular'
| Policy | Ne zaman gerekir | Lab'daki durum |
|---|---|---|
angular | Trusted Types enforce edildiğinde her zaman; Angular'ın incelenmiş iç kodu | @angular/core |
angular#unsafe-bypass | bypassSecurityTrust* method'larını kullanıyorsan | @angular/core |
angular#unsafe-jit | JIT (Just-in-Time) compiler'ı doğrudan kullanıyorsan | @angular/compiler |
angular#unsafe-upgrade | @angular/upgrade ile AngularJS hibrit uygulamaları | paket kurulu değil |
angular#bundler | Angular CLI (Command-Line Interface) bundler'ı lazy chunk dosyaları oluştururken | @angular/build içinde yok |
İki not. angular#bundler adı bugünkü esbuild tabanlı @angular/build paketinde geçmiyor; lab'da lazy loading yapılan bir route'u bu ad olmadan, Trusted Types enforce edilirken sorunsuz açtım. Eski webpack tabanlı builder'dan geçiş yapmamış projelerde rehberdeki listeye göre davran. angular#auto-csp ise autoCsp'nin loader'ında duruyor ama yalnız loader Trusted Types modunda üretilirse kullanılıyor; Angular CLI'ın şu anki autoCsp çıktısında bu mod kapalı. Adlarda # olması, bir uygulamanın ve library'lerin policy'lerini bir arada okunur tutmak için yaygın bir isimlendirme alışkanlığı; kendi policy'lerine de bookstore#... gibi adlar vermek iyi bir fikir.
Report-Only ile Envanter
Mevcut bir uygulamada Trusted Types'ı doğrudan enforce etmek, ilk ihlalde uygulamanın bir kısmını kırmak demek. Önce Report-Only:
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types angular; report-uri /csp-report
Bookstore'un beş sayfasını headless Chrome'da açtım. Önemli satırlar:
[console.log] [csp-violation] directive=require-trusted-types-for blocked=trusted-types-sink disposition=report sample=HTMLScriptElement src|http://127.0.0.1:19424/analytics.js
[console.log] [csp-violation] directive=require-trusted-types-for blocked=trusted-types-sink disposition=report sample=HTMLScriptElement src|http://127.0.0.1:19424/pixel.js
[console.log] [csp-violation] directive=require-trusted-types-for blocked=trusted-types-sink disposition=report sample=Element innerHTML|Great book <img src=x onerror="console.l
[console.log] [csp-violation] directive=require-trusted-types-for blocked=trusted-types-sink disposition=report sample=Element insertAdjacentHTML|Great book <img src=x onerror="console.l
[console.info] Creating a TrustedTypePolicy named 'dompurify' violates the following Content Security policy directive: "trusted-types angular". The policy is report-only, so the violation has been logged but no further action has been taken.
[console.info] Creating a TrustedTypePolicy named 'angular#unsafe-bypass' violates the following Content Security policy directive: "trusted-types angular". The policy is report-only, so the violation has been logged but no further action has been taken.
[console.log] [csp-violation] directive=require-trusted-types-for blocked=trusted-types-sink disposition=report sample=Element innerHTML|Great book <img src="x">
Trusted Types raporlarının sample alanı çok işe yarar: sink'in adı, | ve değerin ilk 40 karakteri. /csp-report'a gelen raporlarda aynı bilgi script-sample olarak, source-file ve satır numarasıyla birlikte duruyor:
[report] application/csp-report {"csp-report":{"document-uri":"http://localhost:19425/sinks","referrer":"","violated-directive":"require-trusted-types-for","effective-directive":"require-trusted-types-for","original-policy":"require-trusted-types-for 'script'; trusted-types angular; report-uri /csp-report","disposition":"report","blocked-uri":"trusted-types-sink","line-number":3,"column-number":11,"source-file":"http://127.0.0.1:19424/analytics.js","status-code":200,"script-sample":"HTMLScriptElement src|http://127.0.0.1:19424/pixel.js"}}
Envanter beş başlığa ayrıldı:
index.html'deki tag manager loader'ıscript.src'ye string yazıyor.- Üçüncü parti
analytics.js, kendi içindepixel.jsiçin aynısını yapıyor; bu kodu değiştiremiyoruz. - Önceki yazının zafiyetli component'i
innerHTMLveinsertAdjacentHTML'e string yazıyor. - "Düzeltilmiş" component bile,
DomSanitizer.sanitizeçıktısını string olarakinnerHTML'e yazdığı için ihlal üretiyor (son satır). - DOMPurify kendi
dompurifypolicy'sini oluşturamıyor; editör sayfasıangular#unsafe-bypass'a ihtiyaç duyuyor.
Lazy loading yapılan route'ta hiçbir ihlal yoktu. Aynı header'ı enforce ettiğimde rapor yerine exception gördüm ve zafiyetli sayfada ilk exception effect'in geri kalanını da durdurdu:
[console.error] ERROR TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
Bu, Report-Only'nin neden ilk adım olduğunu gösteriyor: enforce modunda ilk ihlal ondan sonraki kodu da çalıştırmaz, envanter yarım kalır.
İhlalleri Tek Tek Çözmek
Kendi kodun: template'e dön
En temiz çözüm sink'i hiç kullanmamak. HTML gereken yerde [innerHTML] binding'i Angular'ın angular policy'sinden geçer; metin için interpolation, URL için önceki yazıdaki allowlist ve [href]:
@Component({
selector: 'app-review-tt',
template: `
@for (review of reviews(); track review.author) {
<article>
<p [id]="'html-' + review.author" [innerHTML]="review.text"></p>
<p [id]="'text-' + review.author">{{ review.text }}</p>
@if (url(review.website); as href) {
<a [href]="href">Website</a>
}
</article>
}
`,
})
export class ReviewTrustedTypes {
private readonly http = inject(HttpClient);
protected readonly reviews = toSignal(this.http.get<Review[]>('/api/reviews.json'), { initialValue: [] });
protected readonly url = safeUrl;
}
HTML üreten library: kendi policy'sine izin ver
DOMPurify, Trusted Types olan bir tarayıcıda dompurify adlı bir policy oluşturur ve parse için ona dayanır. Ad listede yokken lab'da hem DOMParser parseFromString hem innerHTML ihlali gördüm ve Markdown alanı boş kaldı. trusted-types listesine dompurify'ı eklemek bunu çözdü. Bu, DOMPurify'ın kendi policy'sine güvenmek demek; bunu bilinçli yapıyorsun, çünkü HTML'i temizleyen kod tam olarak o.
Loader ve değiştiremediğin üçüncü parti kod
Kendi loader'ın için adlandırılmış bir policy yaz; URL'i origin allowlist'ine göre doğrulasın. analytics.js ise senin kodun değil; onun script.src atamasını yakalamanın tek yolu default policy. Tarayıcı, policy'siz bir string bir sink'e geldiğinde default adlı policy varsa onu çağırır. Kritik nokta: bu policy'ye yalnız ihtiyacın olan method'u, yani createScriptURL'ü veriyorum, aynı allowlist ile:
<script>
(() => {
const allowedOrigins = ['http://127.0.0.1:19424'];
const toScriptUrl = (url) => {
const parsed = new URL(url, document.baseURI);
if (allowedOrigins.includes(parsed.origin)) return parsed.href;
throw new TypeError(`Blocked script URL: ${url}`);
};
const tt = window.trustedTypes;
const loader = tt
? tt.createPolicy('bookstore#loader', { createScriptURL: toScriptUrl })
: { createScriptURL: toScriptUrl };
// değiştiremediğimiz üçüncü parti kod: yalnız script URL'leri, aynı allowlist
tt?.createPolicy('default', {
createScriptURL: (url, type, sink) => {
console.warn(`default policy used by ${sink}: ${url}`);
return toScriptUrl(url);
},
});
const tm = document.createElement('script');
tm.src = loader.createScriptURL('http://127.0.0.1:19424/analytics.js');
document.head.append(tm);
})();
</script>
Bu script main.js'ten önce çalışıyor, çünkü default policy'nin üçüncü parti kod yüklenmeden önce var olması gerekiyor. Trusted Types olmayan tarayıcıda loader düz bir object'e düşüyor ve allowlist kontrolü yine yapılıyor. Yeni header:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types angular dompurify bookstore#loader default
bypassSecurityTrust*: önce sor, sonra izin ver
Editörün seçtiği kitabı vurgulayan sayfa bypassSecurityTrustHtml kullanıyordu. angular#unsafe-bypass listede yokken Angular policy'yi oluşturamadı ve atama reddedildi; listeye eklediğimde <b style="color: teal">Editor pick</b> yazıldı. Bu adı eklemeden önce her bypass çağrısının sabit bir değere mi yoksa kullanıcı verisine mi uygulandığını kontrol et; adı bir kez eklediğinde kod tabanındaki bütün bypass çağrıları açılır.
Savunmayı Doğrulamak: Smoke Test, ng serve ve jsdom
Düzeltmelerin işe yaradığını elle değil testle göstermek istiyorum. Playwright ile yazdığım küçük script her route'u açıyor, securitypolicyviolation event'lerini ve sayfa hatalarını topluyor ve ihlal varsa sıfırdan farklı kodla çıkıyor:
// Kullanım: node tt-smoke.mjs <baseUrl> <route> [<route> ...]
import { chromium } from 'playwright-core';
const [baseUrl, ...routes] = process.argv.slice(2);
const browser = await chromium.launch({ executablePath: process.env.CHROME });
let failures = 0;
for (const route of routes) {
const page = await browser.newPage();
const violations = [];
await page.exposeFunction('reportViolation', (v) => violations.push(v));
await page.addInitScript(() => {
document.addEventListener('securitypolicyviolation', (e) =>
window.reportViolation(`${e.effectiveDirective}: ${e.sample}`));
});
page.on('pageerror', (e) => violations.push(`pageerror: ${e.message}`));
await page.goto(new URL(route, baseUrl).href, { waitUntil: 'networkidle' });
await page.waitForTimeout(500);
console.log(`${violations.length ? 'FAIL' : 'PASS'} ${route}`);
for (const v of violations) console.log(` ${v}`);
failures += violations.length ? 1 : 0;
await page.close();
}
await browser.close();
process.exit(failures ? 1 : 0);
Yeni header ile, angular#unsafe-bypass olmadan:
PASS /reviews
PASS /markdown
PASS /lazy
FAIL /bypass
trusted-types: angular#unsafe-bypass
require-trusted-types-for: Element innerHTML|<b style="color: teal">Editor pick</b>
FAIL /sinks
require-trusted-types-for: Element innerHTML|Great book <img src=x onerror="console.l
Script 1 koduyla çıktı. /sinks bilerek bıraktığım zafiyetli sayfa; orada FAIL beklediğimiz sonuç. /reviews sayfasında Mallory'nin yorumu Great book <img src="x"> olarak yazıldı, javascript: link'i hiç oluşmadı. Analytics zinciri de çalıştı ve konsola default policy used by HTMLScriptElement src: http://127.0.0.1:19424/pixel.js uyarısı düştü; o satır, default policy'nin ne zaman devreye girdiğini production loglarında görmenin yolu.
Trusted Types'ı yalnız production'da açarsan ihlalleri production'da öğrenirsin. angular.json'da dev server için header tanımlanabiliyor:
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"headers": {
"Content-Security-Policy": "require-trusted-types-for 'script'; trusted-types angular angular#unsafe-bypass dompurify bookstore#loader default"
}
}
}
ng serve altında Vite'ın istemci script'i bir ihlal üretmedi; Markdown sayfası temiz açıldı, zafiyetli sayfada ise production'daki TypeError'ın aynısını aldım. Böylece geliştirici ihlali kodu yazdığı anda görüyor.
Birim testleri ise farklı bir hikâye. Angular 22'nin yeni projelerde varsayılan test runner'ı Vitest ve DOM ortamı jsdom. jsdom'da Trusted Types yok:
jsdom trustedTypes: undefined | jsdom 30.1.3
Yani jsdom'daki testler bir Trusted Types ihlalini asla yakalamaz. angular.dev, Karma kullanan projeler için karma.config.js'te customHeaders ile header vermeyi anlatıyor. Vitest ile çalışıyorsan bu kontrolü gerçek tarayıcıda koşan bir aşamaya, yukarıdaki gibi bir smoke test'e ya da uçtan uca testlere koy.
default Policy'nin Tehlikesi
default policy'yi "her şey çalışsın" diye yazmak çok cazip. Aynı build'de default policy'ye tek satır ekledim: createHTML: (html) => html. Sonuç:
[console.log] analytics.js loaded
[console.warning] default policy used by HTMLScriptElement src: http://127.0.0.1:19424/pixel.js
[console.log] pixel.js loaded (created by analytics.js)
[console.log] XSS fired in #renderer
[console.log] XSS fired in #adjacent
[console.log] XSS fired in #native
Header aynı, require-trusted-types-for 'script' hâlâ orada; ama her string sessizce TrustedHTML'e çevrildiği için üç sink yeniden açıldı. default policy, Trusted Types'ın kod tabanını "policy'leri incele" seviyesine indiren garantisini geri alır. Kullanacaksan yalnız gerçekten ihtiyaç duyduğun method'u tanımla, değeri doğrula ya da sanitize et, kullanıldığı her yeri logla ve kalıcı bir çözüm değil, üçüncü parti kod düzelene kadar bir köprü olarak gör.
Tarayıcı Durumu ve Geri Düşüş
MDN'in (MDN Web Docs) browser-compat-data paketinin 8.1.8 sürümüne ve web-features verisine göre tablo:
| Tarayıcı | require-trusted-types-for ve trusted-types |
|---|---|
| Chrome ve Edge | 83 |
| Firefox | 148 |
| Safari | 26 |
Firefox 148 ile Trusted Types, 24 Şubat 2026'da "Baseline 2026 newly available" oldu. Yani Kasım 2026'da güncel tarayıcıların hepsi direktifi uyguluyor, ama "widely available" seviyesi için 30 aylık süre henüz dolmadı ve eski sürümlerdeki kullanıcıların var. Desteklemeyen tarayıcı direktifi yok sayar; o kullanıcılar için Angular'ın DomSanitizer'ı yine çalışır, angular.dev bunu açıkça belirtiyor. Trusted Types bu yüzden ek bir katman: kodun doğru yazılmış olmasının yerine geçmez, yanlış yazıldığında bunu tarayıcıda kesin olarak görünür kılar.
Spesifikasyon tarafında Trusted Types, W3C'nin (World Wide Web Consortium) Web Application Security Working Group'unda geliştiriliyor; son yayımlanan sürümü 7 Ekim 2026 tarihli bir Working Draft. Yani tarayıcıların üçü de uyguluyor, ama metin hâlâ Recommendation yolunda ve ayrıntılar değişebilir.
Üretimde Dikkat
Trusted Types'ı strict CSP ile birlikte düşün. CSP hangi script'in çalışacağını, Trusted Types hangi değerin sink'e ulaşacağını sınırlar. 'strict-dynamic' güvendiğin bir script'in eklediği her script'e güvendiği için script.src'yi Trusted Types ile sınırlamak bu zinciri kontrol altına alır.
Envanteri production trafiğiyle çıkar. Report-Only'yi en az bir iş döngüsü boyunca açık tut; raporları script-sample'daki sink adına göre grupla. Rapor sample'ları değerin ilk 40 karakterini taşır; log saklama politikanı buna göre düşün, çünkü bir HTML parçasında kullanıcı verisi olabilir.
Policy listesini küçük tut ve her adın bir sahibi olsun. trusted-types direktifine 'allow-duplicates' ya da * eklemek, policy adını kısıtlamanın faydasını azaltır. Yeni bir ad eklenmesi code review'da ayrıca onaylanmalı.
Sık Düşülen Tuzaklar
Birincisi, DomSanitizer.sanitize sonucunu innerHTML'e yazıp güvende olduğunu sanmak. Değer temiz ama tipi string; Trusted Types açıkken bu atama reddedilir. Template binding'ine dön.
İkincisi, ihlalleri default policy ile topluca susturmak. Lab'da tek satırlık createHTML: (html) => html üç XSS'i geri getirdi.
Üçüncüsü, default policy'yi Angular'ın bootstrap'ından sonra tanımlamak. Üçüncü parti script'ler policy'den önce yüklenirse ilk atama yine reddedilir.
Dördüncüsü, jsdom'daki yeşil testlere güvenmek. jsdom'da trustedTypes yok; ihlal yalnız gerçek tarayıcıda görünür.
Beşincisi, Trusted Types'ı enforce'a doğrudan geçirmek. Enforce modunda ilk exception sonraki kodu durdurduğu için envanter yarım kalır ve kullanıcı kırık bir sayfa görür.
Küçük Bir Deneme
★☆☆ Lab'daki header'dan dompurify'ı çıkar ve Markdown sayfasını smoke test ile aç. Hangi sink'lerin raporlandığını not et ve sonra adı geri ekle.
★★☆ bookstore#loader policy'sindeki allowlist'e olmayan bir origin'den script yüklemeyi dene. toScriptUrl'ün fırlattığı hatanın konsolda ve smoke test'te nasıl göründüğüne bak.
★★★ default policy'ye bir createHTML ekle, ama değeri DOMPurify ile temizleyip döndürsün ve her çağrıyı sink adıyla logla. Zafiyetli sayfayı aç: XSS çalışıyor mu, hangi sink'ler loglanıyor? Bu yaklaşımın neden yine de kalıcı bir çözüm olmadığını not et.
Strict CSP ve Trusted Types ile tarayıcıdaki iki savunma hattı hazır. Bir sonraki yazıda farklı bir soruya geçiyoruz: XSS bir gün yine olursa saldırgan token'ına ulaşabilir mi? localStorage, memory, cookie, service worker ve BFF (Backend for Frontend) seçeneklerinin tehdit modelini çıkarıyoruz.