Content Security Policy fără să vă stricați site-ul
Construiți un CSP care oprește scripturile injectate: lansare report-only, nonce și strict-dynamic, directivele care contează și remedierea încălcărilor.
Ce face CSP
Cross-site scripting (XSS) apare atunci când un atacator reușește să ruleze propriul JavaScript în pagina dumneavoastră, de exemplu printr-un câmp de comentarii sau printr-un parametru de URL afișat fără escapare. Browserul nu poate deosebi acel script de al dumneavoastră. Content Security Policy, specificat de W3C, vă permite să îi spuneți browserului care surse de script și de alt conținut sunt legitime.
CSP este a doua linie de apărare. Escaparea ieșirilor și igienizarea intrărilor rămân remediul principal pentru XSS; CSP limitează pagubele atunci când o eroare scapă. Controlează totodată încadrarea site-ului în frame-uri, unde pot trimite formularele și dacă pagina poate încărca conținut mixt.
Directivele care contează
| Directivă | Ce controlează | Valoare tipică |
|---|---|---|
default-src | Rezervă pentru directivele fetch nelistate | 'self' |
script-src | Surse de JavaScript | 'nonce-…' 'strict-dynamic' |
style-src | Foi de stil și stiluri inline | 'self' (plus nonce pentru stilurile inline) |
img-src | Imagini | 'self' data: |
connect-src | Ținte fetch, XHR, WebSocket | 'self' https://api.example.com |
font-src | Fonturi web | 'self' |
frame-src | Frame-urile pe care le încorporează pagina | Gazde specifice sau 'none' |
object-src | Pluginuri precum <object> și <embed> | 'none' |
base-uri | Elementul <base> | 'none' sau 'self' |
form-action | Unde pot trimite formularele | 'self' |
frame-ancestors | Cine vă poate încadra pagina în frame (clickjacking) | 'self' sau 'none' |
upgrade-insecure-requests | Rescrie subresursele http:// în https:// | (fără valoare) |
frame-ancestors, sandbox și directivele de raportare funcționează doar în antetul HTTP, nu într-o etichetă <meta http-equiv>. Folosiți antetul ori de câte ori puteți. frame-ancestors înlocuiește în browserele actuale și vechiul antet X-Frame-Options, deși trimiterea ambelor nu face rău.
Liste de gazde permise versus nonce
Primele implementări CSP listau gazdele de script permise: script-src 'self' https://cdn.example.net https://analytics.example.org. Cercetările unor ingineri Google au arătat că majoritatea acestor politici pot fi ocolite, pentru că CDN-urile și gazdele de analytics permise servesc adesea scripturi sau endpointuri JSONP de care un atacator poate abuza. Listele lungi sunt și fragile: fiecare script terț nou cere o modificare a politicii.
O politică bazată pe nonce acordă încredere scripturilor după o valoare aleatoare, nu după locație. Serverul generează pentru fiecare răspuns un nonce nou, imposibil de ghicit, îl adaugă în antet și în fiecare etichetă <script> legitimă. Un script injectat nu cunoaște nonce-ul, deci nu rulează.
Content-Security-Policy: script-src 'nonce-4AEemGb0xJptoIGFP3Nd' 'strict-dynamic'; object-src 'none'; base-uri 'none'
<script nonce="4AEemGb0xJptoIGFP3Nd" src="/assets/app.js"></script>
<script nonce="4AEemGb0xJptoIGFP3Nd">window.appConfig = { locale: "en" };</script>'strict-dynamic' extinde această încredere către scripturile încărcate de un script de încredere, cum ar fi un tag manager care inserează alte etichete, fără a le lista gazdele. Browserele care acceptă 'strict-dynamic' ignoră listele de gazde și 'unsafe-inline' din aceeași directivă, ceea ce vă permite să le adăugați ca rezervă pentru browsere foarte vechi, fără a slăbi politica pentru cele moderne.
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
frame-ancestors 'self';
report-to cspNonce-ul trebuie să fie unic pentru fiecare răspuns
Hash-uri pentru pagini statice
Site-urile statice, fără randare pe server, nu pot adăuga un nonce nou la fiecare răspuns. În schimb, listați hash-ul SHA-256 al conținutului exact al fiecărui script inline, de forma 'sha256-…', sau mutați scripturile inline în fișiere servite de pe propria origine. Scripturile externe încărcate din 'self' nu mai au atunci nevoie de niciun nonce.
Lansarea fără a strica nimic
- Inventar. Listați scripturile, stilurile, fonturile, frame-urile și endpointurile API folosite de site, inclusiv terții precum analytics, widgeturile de chat și formularele de plată.
- Schițați-o. Nu trebuie să scrieți de mână prima versiune. Verificatorul de antete HTTP are un generator sub rezultatul Content-Security-Policy: numiți originile de la care pagina încarcă fiecare tip de resursă, spuneți dacă are nevoie de stiluri inline sau de scripturi inline, iar el scrie antetul, linia pentru nginx și linia pentru Apache, apoi trece politica produsă înapoi prin aceeași verificare, astfel încât o slăbiciune a ceea ce a generat să fie raportată chiar lângă ea. Pornește intenționat în modul report-only.
- Report-only. Trimiteți politica drept
Content-Security-Policy-Report-Only. Browserele raportează încălcările, dar nu blochează nimic. - Colectați rapoarte. Configurați un endpoint de raportare și urmăriți consola browserului pe paginile și fluxurile importante.
- Reparați pagina, nu doar politica. Mutați handlerele de evenimente inline (
onclick=) în fișiere de script, adăugați nonce scripturilor inline legitime, eliminați terții nefolosiți. - Aplicați. Treceți la
Content-Security-Policydupă ce rapoartele arată doar extensii de browser și zgomot. Păstrați antetul report-only cu o politică candidat mai strictă, dacă vreți să strângeți și mai mult. - Supravegheați. Lăsați raportarea activă; funcțiile noi și schimbările la terți vor produce încălcări noi.
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; report-to cspreport-to, împreună cu antetul Reporting-Endpoints, este mecanismul actual. Vechea directivă report-uri este depreciată, dar încă înțeleasă de browserele care nu acceptă report-to, așa că multe site-uri le trimit pe amândouă în perioada de tranziție. Rapoartele sunt JSON; așteptați-vă la zgomot de la extensiile de browser care injectează scripturi și filtrați după blocked-uri și source-file.
Încălcări frecvente și remedii
| Încălcare | Cauză | Remediu |
|---|---|---|
| Script inline blocat | <script> fără nonce | Adăugați nonce-ul specific răspunsului sau mutați codul într-un fișier |
| Handler de eveniment inline blocat | Atribute onclick="…" | Atașați ascultători în JavaScript cu addEventListener |
URL javascript: blocat | Linkuri precum href="javascript:…" | Folosiți un buton cu un ascultător |
eval blocat | Biblioteci care folosesc eval sau new Function | Actualizați biblioteca; 'unsafe-eval' este ultima soluție |
| Stil blocat | Atribute style= inline sau blocuri <style> | Mutați în foi de stil sau puneți nonce pe elementul <style> |
| Conexiune blocată | Endpoint API sau de analytics absent din connect-src | Adăugați originea exactă |
| Frame blocat | Video, hartă sau formular de plată încorporat | Adăugați gazda în frame-src |
Remediați încălcările în ordinea riscului: mai întâi cele de script, apoi conexiunile și frame-urile, iar stilurile la final. Sursele de script decid dacă un cod injectat poate rula, în timp ce majoritatea încălcărilor de stil sunt cosmetice.
Nu adăugați 'unsafe-inline' în script-src fără nonce sau hash-uri. Permite exact scripturile inline injectate pe care CSP ar trebui să le oprească, iar Verificatorul de antete HTTP îl raportează ca politică slabă. 'unsafe-inline' în style-src este un risc mai mic și un compromis obișnuit cât timp stilurile inline sunt curățate.
Un exemplu practic: strângerea unei politici reale
Luați un site de prezentare la example.com care a acumulat o politică de-a lungul anilor. Funcționează, dar Verificatorul de antete HTTP o marchează drept slabă, pentru că script-src conține 'unsafe-inline' și un wildcard. Iată punctul de plecare și politica la care poate ajunge în câteva iterații.
Content-Security-Policy: default-src 'self' https:; script-src 'self' 'unsafe-inline' https://*.example.net https://analytics.example.org; style-src 'self' 'unsafe-inline'; img-src * data:Problemele sunt ușor de numit. 'unsafe-inline' lasă să ruleze orice script inline injectat, https://*.example.net are încredere în fiecare gazdă de sub un domeniu CDN comun, default-src https: permite orice origine HTTPS pentru tot ce nu este listat, iar img-src * permite imagini de oriunde, ceea ce poate scurge date prin URL-urile imaginilor. Lipsesc de asemenea object-src, base-uri și frame-ancestors.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://analytics.example.org;
connect-src 'self' https://analytics.example.org;
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'self';
report-to cspCa să ajungă acolo au fost nevoie de trei modificări la site, nu la politică: handlerele onclick inline au fost mutate în scriptul principal, loaderul de analytics a primit nonce-ul, iar un widget vechi care avea nevoie de eval a fost înlocuit. 'unsafe-inline' în style-src rămâne deocamdată, ca un compromis documentat. În browserele care acceptă nonce, intrările https: și 'unsafe-inline' din script-src sunt ignorate, deci doar mențin funcționale browserele foarte vechi.
| Iterație | Schimbare | Rezultat report-only |
|---|---|---|
| 1 | S-au adăugat object-src 'none'; base-uri 'none'; frame-ancestors 'self' | Nicio încălcare |
| 2 | Lista de gazde a fost înlocuită cu nonce și 'strict-dynamic' | Au fost raportate handlere inline și un eval |
| 3 | Handlerele au fost mutate în script, widgetul cu eval a fost înlocuit | Doar zgomot de la extensii de browser |
| 4 | S-au strâns default-src, img-src și connect-src | Două endpointuri de analytics adăugate, apoi curat |
Dincolo de scripturi
Clickjacking
frame-ancestors 'none' împiedică orice site să vă încadreze pagina în frame; 'self' permite doar propriile pagini. Înlocuiește X-Frame-Options: DENY și SAMEORIGIN în browserele moderne.
Conținut mixt
upgrade-insecure-requests face browserul să încarce prin HTTPS imaginile, scripturile și stilurile http://. Ajută la migrarea conținutului vechi, împreună cu HSTS pentru pagina însăși.
Trusted Types
require-trusted-types-for 'script' face browserele compatibile să respingă șirurile trimise către puncte DOM periculoase precum innerHTML, dacă nu provin dintr-o politică aprobată. Vizează XSS-ul din DOM pe care nonce-urile nu îl pot prinde. Cere modificări de cod și este cel mai bine adăugat după ce politica principală s-a stabilizat.
Menținerea politicii în formă
Un CSP nu este un proiect de o singură dată. Fiecare funcție nouă, unealtă de marketing sau widget încorporat poate cere o modificare a politicii, iar o politică fără proprietar tinde să adune excepții până când nu mai protejează nimic. Dați politicii un proprietar, țineți-o în controlul versiunilor lângă aplicație și tratați modificările ei ca pe niște modificări de cod.
Două obiceiuri o mențin strânsă. Mai întâi, când adăugați o sursă, preferați forma cea mai îngustă: o origine exactă în connect-src în loc de un wildcard și un nonce în loc de o gazdă de script nouă. Apoi, revizuiți lunar rapoartele de încălcare: o creștere bruscă înseamnă adesea că a fost adăugat un script terț fără să anunțe nimeni sau că o tentativă de injecție este blocată, iar ambele merită știute.
În final, testați politica ca parte din procesul de livrare. O verificare simplă, care cere paginile importante și oprește pipeline-ul când antetul Content-Security-Policy lipsește sau conține 'unsafe-inline' fără nonce, prinde regresiile accidentale, cum ar fi o schimbare de configurație la proxy care elimină antetul.
CSP cu framework-uri, CDN-uri și tag manager
Framework-urile care randează pe server au de obicei o modalitate de a injecta un nonce în fiecare etichetă de script pe care o generează; căutați suportul pentru CSP sau nonce în documentația framework-ului. Aplicațiile single-page compilate în fișiere statice funcționează bine cu o politică ce permite scripturi din 'self' și nu are deloc scripturi inline. Uneltele de build pot fi de multe ori configurate să evite scripturile de bootstrap inline.
CDN-urile și platformele edge pot adăuga sau rescrie antete, ceea ce este comod, dar poate lăsa și două politici diferite în vigoare. Când sunt prezente două antete CSP, browserul le aplică pe amândouă, iar o resursă trebuie permisă de fiecare politică. Verificați răspunsul final cu Verificatorul de antete HTTP după fiecare livrare.
Tag managerele sunt cazul cel mai greu, pentru că echipele de marketing adaugă scripturi fără modificări de cod. Cu 'strict-dynamic', scripturile inserate de un tag manager de încredere prin nonce sunt permise, ceea ce ține lucrurile funcționale, dar înseamnă și că oricine are acces la tag manager poate rula cod pe site-ul dumneavoastră. Protejați acel acces ca pe drepturile de livrare în producție.
Întrebări frecvente
Pot folosi CSP într-o etichetă meta?
Parțial. <meta http-equiv="Content-Security-Policy"> acceptă majoritatea directivelor fetch, dar nu și frame-ancestors, sandbox sau raportarea. Antetul HTTP este opțiunea completă.
Înlocuiește CSP escaparea ieșirilor?
Nu. Escaparea și igienizarea opresc XSS la sursă; CSP limitează impactul atunci când ceva scapă.
De ce îmi blochează politica Google Analytics sau un widget de chat?
Scripturile lor încarcă alte scripturi și se conectează la propriile endpointuri. Folosiți un nonce cu 'strict-dynamic' pentru loader și adăugați endpointurile necesare în connect-src și img-src, așa cum le listează documentația lor.
Care este diferența dintre report-uri și report-to?
report-uri este directiva veche, depreciată, care primește un URL. report-to face referire la un endpoint numit în antetul Reporting-Endpoints. Trimiterea ambelor păstrează fluxul de rapoarte din toate browserele.
Este periculos 'unsafe-inline' în style-src?
Mult mai puțin decât în script-src, dar stilurile injectate pot fi totuși folosite pentru trucuri de exfiltrare a datelor sau pentru falsificarea interfeței. Tratați-l ca pe un compromis temporar.
Pot folosi același nonce pentru toate scripturile dintr-o pagină?
Da. Un nonce per răspuns este împărțit de fiecare etichetă de script legitimă din acel răspuns. Ce nu trebuie să se întâmple este ca același nonce să fie reutilizat între răspunsuri sau utilizatori, de exemplu printr-un cache de pagină.
Încetinește un CSP site-ul?
Nu într-o măsură perceptibilă pentru utilizatori. Browserul verifică fiecare resursă față de politică, iar asta este ieftin. Efortul este de partea echipei: menținerea politicii la zi cu funcțiile noi și cu terții.
Ar trebui ca un API care returnează doar JSON să trimită un CSP?
Nu strică și protejează împotriva unui răspuns randat din greșeală ca HTML. Un minim default-src 'none'; frame-ancestors 'none' este obișnuit pentru API-uri.
Cum gestionez zgomotul de la extensiile de browser din rapoarte?
Extensiile injectează scripturi și stiluri, care apar ca încălcări cu valori blocked-uri precum chrome-extension sau cu surse inline pe care nu le recunoașteți. Filtrați-le când rezumați rapoartele; nu sunt probleme în site-ul dumneavoastră.
Cât timp ar trebui să ruleze report-only?
Până când toate paginile și fluxurile importante au fost parcurse, iar rapoartele arată doar zgomot, de obicei între una și câteva săptămâni pentru un site activ.