Angular pornește de la o regulă simplă: orice valoare legată în template e considerată nesigură, până la proba contrarie. Interpolarea escapează textul. [innerHTML] trece prin sanitizer. URL-urile cu scheme periculoase sunt neutralizate. De aceea un XSS într-o aplicație Angular apare aproape mereu când cineva ocolește aceste protecții, nu când le folosește.
Pagina arată ce face Angular automat, unde sunt „ușile laterale” pe care le cauți la code review și cum adaugi a doua linie de apărare: Content Security Policy și Trusted Types. La final e protecția XSRF din HttpClient.
localStorage. Protecțiile implicite acoperă cazurile obișnuite. Restul depinde de ce faci tu cu bypassSecurityTrust* și cu DOM-ul.Contextele de securitate
Angular alege tratamentul după unde ajunge valoarea. În text o escapează. În HTML ([innerHTML]) o sanitizează: păstrează tagurile de formatare și elimină <script>, atributele on*, <iframe> și URL-urile periculoase. La URL ([href], [src]) neutralizează scheme ca javascript:. Resource URL (sursa unui <iframe> sau a unui script) e cel mai strict context, pentru că acolo un URL înseamnă cod executat.
<!-- Interpolare: textul e ESCAPAT — „<b>” apare literal, nu devine tag -->
<p>{{ comment.text }}</p>
<!-- [innerHTML]: HTML-ul e SANITIZAT — rămân tagurile de formatare, dispar script-urile și handler-ele -->
<div [innerHTML]="comment.html"></div>
<!-- URL-uri: javascript: și alte scheme periculoase devin „unsafe:…” -->
<a [href]="profile.website">site</a>
<!-- Stiluri: valorile din [style] sunt și ele verificate -->
<div [style.background-image]="'url(' + user.avatar + ')'"></div>[innerHTML] (sanitizat automat) Salut, lume! Vezi docs.
sanitizer.sanitize(SecurityContext.HTML, …)<p><b>Salut</b>, <em>lume</em>! Vezi <a href="https://angular.dev">docs</a>.</p>
nimic eliminat conținutul era deja sigur.
Cu bypassSecurityTrustHtml(…) Angular ar insera exact textul din căsuță, inclusiv handler-ele de evenimente. De aceea demo-ul nu face asta.
URL-uri
Un link cu href venit de la utilizator („site-ul meu”) e un vector clasic de atac: javascript:alert(document.cookie) rulează cod la click. Angular prefixează valorile nesigure cu unsafe:, deci browserul nu le mai poate executa. Schemele obișnuite (https:, mailto:, căi relative) trec neschimbate.
Template: <a [href]="url()"> → atributul din DOM:
bypassSecurityTrust și ușile laterale
DomSanitizer are cinci metode bypassSecurityTrust… (Html, Script, Style, Url, ResourceUrl). Ele marchează o valoare ca sigură, iar Angular nu o mai verifică. Sunt legitime doar pentru conținut pe care îl construiești și controlezi tu, de exemplu URL-ul unui embed format dintr-un id validat. Pe date de la utilizator sau de la un API extern, fiecare apel e o vulnerabilitate. Documentația Angular le marchează ca „security sensitive”: caută-le la fiecare review.
import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-browser';
export class Embed {
private readonly sanitizer = inject(DomSanitizer);
// ✓ Doar pentru conținut pe care îl CONTROLEZI: un URL construit de tine dintr-un id validat
readonly video: SafeResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
`https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId())}`,
);
// ✗ Niciodată pe ce vine de la utilizator sau de la un API extern
unsafe(html: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(html); // XSS garantat dacă html conține un <img onerror>
}
}Mai periculoase sunt căile care nu trec deloc prin Angular: nativeElement.innerHTML, Renderer2.setProperty(…, 'innerHTML', …), document.write, eval și template-urile construite din string-uri la runtime.
// Toate astea OCOLESC sanitizarea Angular — evită-le cu date din afară:
this.el.nativeElement.innerHTML = userHtml; // DOM direct
this.renderer.setProperty(this.el.nativeElement, 'innerHTML', userHtml);
document.write(userHtml);
new Function(userCode)(); eval(userCode);
// și template-uri construite din string-uri cu input de la utilizator (compilare JIT la runtime)
// ✓ Alternativa: [innerHTML] (sanitizat) sau, pentru markdown, o bibliotecă de randare + sanitizareContent Security Policy
CSP e un header prin care îi spui browserului ce scripturi și stiluri are voie să ruleze. Chiar dacă un atacator reușește să injecteze HTML, scriptul lui nu rulează fără nonce-ul sau hash-ul corect. Angular adaugă stiluri inline (stilurile componentelor), așa că are nevoie de nonce. Îl primește fie prin atributul ngCspNonce pe elementul root (serverul îl generează la fiecare răspuns), fie prin token-ul CSP_NONCE.
Alternativa fără server de template-uri e security.autoCsp. CLI-ul calculează la build hash-urile scripturilor inline și pune politica într-un <meta>, deci index.html rămâne cache-uibil. Documentația îi listează limitele: acoperă doar scripturile, unele directive (de ex. frame-ancestors) tot trebuie trimise în header și nu funcționează cu SSR.
import { CSP_NONCE } from '@angular/core';
// Varianta 1 (server-side templating): serverul pune același nonce în header și în index.html
// <app-root ngCspNonce="r4nd0m-per-request"></app-root>
// Varianta 2: nonce-ul e disponibil la runtime și vrei să poți cache-ui index.html
bootstrapApplication(App, {
providers: [{ provide: CSP_NONCE, useValue: globalThis.myRandomNonceValue }],
});Trusted Types
Trusted Types merg un pas mai departe: browserul refuză orice scriere în „sink-uri” periculoase (innerHTML, src de script…) care nu vine printr-o politică aprobată. Angular are politicile lui: angular pentru ce face framework-ul, angular#unsafe-bypass dacă folosești bypassSecurityTrust*, angular#bundler pentru chunk-urile lazy ale CLI-ului. Un innerHTML scris direct în cod devine eroare în loc de vulnerabilitate.
# Minimul pentru o aplicație Angular
Content-Security-Policy: trusted-types angular; require-trusted-types-for 'script';
# Dacă folosești bypassSecurityTrust*
Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for 'script';
# Dacă ai chunk-uri lazy create de CLI
Content-Security-Policy: trusted-types angular angular#bundler; require-trusted-types-for 'script';Content-Security-Policy: script-src 'self' 'nonce-RANDOM_PER_RESPONSE'; style-src 'self' 'nonce-RANDOM_PER_RESPONSE'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; trusted-types angular angular#bundler; require-trusted-types-for 'script';
<!-- index.html: serverul înlocuiește valoarea la fiecare răspuns -->
<app-root ngCspNonce="RANDOM_PER_RESPONSE"></app-root>
// sau, dacă nonce-ul e cunoscut la runtime:
{ provide: CSP_NONCE, useValue: globalThis.myNonce }XSRF / CSRF
Dacă autentificarea se face cu cookie de sesiune, o pagină străină poate trimite un formular către API-ul tău, iar browserul atașează cookie-ul automat. HttpClient are protecția integrată: citește cookie-ul XSRF-TOKEN și trimite valoarea în header-ul X-XSRF-TOKEN. Doar codul de pe domeniul tău poate citi cookie-ul, deci serverul știe de unde vine cererea. Header-ul se pune doar pe request-urile care modifică date (nu pe GET/HEAD), către URL-uri relative sau de pe același origin. Partea de server e obligatorie: serverul setează cookie-ul și verifică header-ul.
import { provideHttpClient, withXsrfConfiguration, withNoXsrfProtection } from '@angular/common/http';
// Implicit: HttpClient citește cookie-ul XSRF-TOKEN și trimite header-ul X-XSRF-TOKEN
// pe request-urile care MODIFICĂ date (POST, PUT, DELETE…) către URL-uri relative / același origin.
provideHttpClient(
withXsrfConfiguration({ cookieName: 'CSRF-TOKEN', headerName: 'X-CSRF-TOKEN' }), // dacă backend-ul folosește alte nume
);
// Dezactivare (ex. API cu token Bearer, fără cookie de sesiune):
provideHttpClient(withNoXsrfProtection());Când DA, când NU
- Lasă Angular să facă binding-ul: interpolare și
[innerHTML], nu DOM direct. - CSP strict (nonce sau
autoCsp) și Trusted Types în producție. - Cookie-uri de sesiune
HttpOnly,Secure,SameSiteși protecția XSRF activă (vezi Autentificare). Pentru login printr-un furnizor extern, vezi OAuth 2.1 / OIDC cu PKCE. - Actualizează Angular și dependențele: corecturile de securitate vin în versiuni patch.
bypassSecurityTrustHtmlpe conținut de la utilizatori sau din CMS-uri externe.- Validare „de securitate” doar în frontend: orice regulă trebuie verificată din nou pe server.
- Token-uri de acces în
localStoragefără CSP: un singur XSS le poate citi. - Dezactivarea warning-urilor de sanitizare „ca să nu mai apară în consolă”: ele îți arată exact unde intră HTML nesigur.
Capcane frecvente
- „Pipe-ul
safeHtml” din proiect, care apeleazăbypassSecurityTrustHtmlpe orice: e un XSS reutilizabil. Șterge-l sau limitează-l la surse sigure. - Markdown randat cu
bypassSecurityTrustHtml: markdown-ul poate conține HTML. Randează-l, apoi trece rezultatul prin sanitizare. - Atribute
hrefconstruite prin concatenare înnativeElement: ocolesc sanitizarea URL-urilor. - CSP adăugat „la final” fără testare: chunk-urile lazy sau stilurile inline sunt blocate și aplicația se strică în producție. Pornește cu
Content-Security-Policy-Report-Only. - XSRF dezactivat cu cookie de sesiune activ: orice site poate face POST-uri în numele utilizatorului.
Fișă rapidă
IMPLICIT (fără să faci nimic)
{{ x }} escapat
[innerHTML] sanitizat (dispar script, on*, javascript:, iframe…) — warning în dev când elimină ceva
[href] / [src] URL-uri periculoase → "unsafe:…"
OCOLIRI (doar pentru conținut controlat de tine — caută-le la code review)
bypassSecurityTrustHtml / Url / ResourceUrl / Style / Script
nativeElement.innerHTML, Renderer2.setProperty('innerHTML'), document.write, eval, template-uri din string-uri
CSP
ngCspNonce="…" pe root sau { provide: CSP_NONCE, useValue } — nonce per răspuns
"security": { "autoCsp": true } — hash-uri la build, index.html cache-uibil, NU cu SSR, doar script-src
TRUSTED TYPES
trusted-types angular [angular#unsafe-bypass] [angular#bundler]; require-trusted-types-for 'script'
XSRF
XSRF-TOKEN → X-XSRF-TOKEN, doar request-uri care modifică, doar same-origin · withXsrfConfiguration · withNoXsrfProtection
SERVER
validează tot din nou · cookie-uri HttpOnly + Secure + SameSite · npm audit / actualizări Angular