frontend-spa
Skill4 starsChanged 10 days agoMITView on GitHub
Sicurezza delle app a pagina singola e dei siti statici (React, Vite, Vue, Svelte) - CSP senza script inline, punti di XSS, link sicuri, token nel browser, variabili pubbliche, source map, service worker, dipendenze. Trigger - "React", "Vite", "SPA", "CSP", "XSS", "dangerouslySetInnerHTML", "sito statico", "localStorage token". EN - Security of single-page apps and static sites (React, Vite, Vue, Svelte) - CSP without inline scripts, XSS sinks, safe links, tokens in the browser, public env vars, source maps, service workers, dependencies. Triggers - "React", "Vite", "SPA", "CSP", "XSS", "dangerouslySetInnerHTML", "static site", "localStorage token".
- Reads credentials
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 143
Never 'unsafe-inline' or 'unsafe-eval' in script-src: a library that needs eval gets replaced or used precompiled.
- Distinctive: line 174
If a library only accepts HTML, the text goes through one shared escape function, never hand-made concatenation:
- Very specific: line 189
Never free user CSS in style or a <style> tag: it can cover the UI with a fake sign-in form or read attributes through selectors.
- Very specific: line 203
Never define: { 'process.env': process.env } in vite.config nor a widened envPrefix: they put the whole build environment in the bundle.
- Distinctive: line 227
Keep a self-disabling worker ready, to ship if a faulty one is out there:
- Very specific: line 237
Before adding a package: recent releases, known maintainers, repository not archived, no deprecated warning.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Frontend e SPA sicuri · Secure Front-end and SPAs
🇮🇹 Versione italiana qui sotto · 🇬🇧 English version below
In una single-page app su hosting statico (React, Vite, Vue, Svelte) non c'è un server dell'app: tutto il codice gira nel browser, chiunque può leggerlo, e i dati sono protetti solo dalle regole del backend (supabase-sicuro, api-sicura). Il frontend ha un compito suo: non far girare codice altrui nella pagina. Con Supabase JS la sessione sta in localStorage, quindi un XSS equivale a prendersi l'account: lo script dell'attaccante legge il refresh token e lo usa da un altro computer. Header generali e HTTPS sono in hardening-siti; qui c'è ciò che è specifico dei frontend statici.
Regole obbligatorie
1. CSP stretta, senza script inline
- Su un hosting statico i nonce sono impossibili (serve un valore nuovo a ogni risposta) e gli hash vanno ricalcolati a ogni build. La via semplice: nessuno script inline, solo file esterni e
script-src 'self'. Lo script che applica il tema prima del primo disegno va in un file (/theme-init.js) caricato nel<head>. - Mai
'unsafe-inline'o'unsafe-eval'inscript-src: una libreria che chiedeevalsi cambia o si usa precompilata. Per WebAssembly basta'wasm-unsafe-eval'.'unsafe-inline'instyle-srcè accettabile solo se una libreria inserisce tag<style>; gli stili di React (style={{…}}) passano comunque. connect-srccon domini esatti, maihttps:o*.supabase.co: il secondo lascia spedire i dati rubati al progetto Supabase dell'attaccante. Le mappe caricano tile, stili e glifi confetch(quindiconnect-src) e spesso avviano worker dablob:.img-srcsolo dai propri domini: un'immagine con URL scelto da un utente fa chiamare a ogni lettore il server dell'attaccante, che riceve l'IP (cioè la zona). Esempio per React + Supabase + mappa:
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob: https://REF.supabase.co;
connect-src 'self' https://REF.supabase.co wss://REF.supabase.co https://tiles.openfreemap.org;
worker-src 'self' blob:; font-src 'self'; manifest-src 'self'; frame-src 'none'; object-src 'none';
base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; report-to cspreport-to cspvuole l'headerReporting-Endpoints: csp="https://…"(un servizio di raccolta o una Edge Function con limite di frequenza); aggiungerereport-uriper i browser che non supportanoreport-to. Una policy nuova va prima pubblicata comeContent-Security-Policy-Report-Onlyper una settimana, leggendo i rapporti. In locale, la stessa CSP inpreview.headersdivite.configmostra gli errori convite preview. Il tag<meta http-equiv>ignoraframe-ancestors,report-uriesandbox: solo dove l'hosting non permette header.
2. Header sull'hosting statico
- Gli header stanno nel file della piattaforma, versionato con il codice, e devono valere anche per la riscrittura SPA (
/*→/index.html). Render (render.yaml):
headers:
- path: /*
name: Content-Security-Policy
value: "default-src 'self'; script-src 'self'; …"- Netlify:
public/_headerscon la riga/*e sotto, indentato,Content-Security-Policy: …. Vercel:vercel.jsoncon{"headers":[{"source":"/(.*)","headers":[{"key":"Content-Security-Policy","value":"…"}]}]}. index.htmlconCache-Control: no-cache, così una correzione arriva a tutti al caricamento successivo; i file con hash in/assets/*conpublic, max-age=31536000, immutable.- Con la CSP:
X-Frame-Options: DENY,Strict-Transport-Security,X-Content-Type-Options: nosniff,Referrer-PolicyePermissions-Policycon solo ciò che l'app usa (geolocation=(self)per la mappa degli amici,camera=(self)per le storie, il resto()) (hardening-siti).
3. Punti di iniezione (sink)
- React fa l'escape del testo in JSX. Le eccezioni vanno cercate a ogni revisione:
dangerouslySetInnerHTML,ref.current.innerHTML,insertAdjacentHTML,document.write,eval/new Function/setTimeout("…"),location.href = …; negli altri frameworkv-html,{@html},bypassSecurityTrust…. Le regole ESLintreact/no-dangereno-restricted-properties(suinnerHTML,outerHTML) segnalano quelli nuovi nella CI. - Le librerie non React scrivono HTML: popup delle mappe (
popup.setHTML()in MapLibre e Mapbox,bindPopup("…")in Leaflet), tooltip dei grafici con formatter che restituiscono HTML, toast. Nomi di tavoli, eventi, gruppi e profili si passano come testo:
const el = document.createElement('div');
el.textContent = evento.titolo; // testo, mai HTML
new maplibregl.Popup().setDOMContent(el).setLngLat(pos).addTo(map);- Se una libreria accetta solo HTML, il testo passa da una funzione di escape condivisa, mai da concatenazioni a mano:
export const esc = (s) => String(s ?? '').replace(/[&<>"']/g,
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);4. URL e CSS scritti dagli utenti
- Un
hrefscelto da un utente (sito nel profilo, link in chat) può esserejavascript:. Non contare sul framework: controllare lo schema con il parser e accettare solohttps:emailto:. Per gli URL mai regex ostartsWith('http'). Lo stesso persrc,window.openelocation.assign.
export function urlSicuro(raw) {
try { const u = new URL(raw); return ['https:', 'mailto:'].includes(u.protocol) ? u.href : null; }
catch { return null; }
}- Link esterni con
target="_blank" rel="noopener noreferrer"(contenuti degli utenti: anchenofollow ugc). Immagini, video e audio degli utenti solo dal proprio Storage, mai URL esterni liberi. Iframe con URL degli utenti: mai. - Mai CSS libero degli utenti in
styleo in un tag<style>: può coprire l'interfaccia con un finto modulo di accesso o leggere attributi con i selettori. Colori e temi da un elenco o da un formato controllato (/^#[0-9a-f]{6}$/i).
5. Markdown, HTML degli utenti e Trusted Types
react-markdownsenzarehype-rawnon rende l'HTML grezzo; se serve, aggiungererehype-sanitize. Con librerie che producono una stringa (marked,markdown-it) sanificare con DOMPurify al momento di mostrarla, non solo al salvataggio, perché il dato può essere stato scritto chiamando l'API direttamente:DOMPurify.sanitize(marked.parse(testo), { USE_PROFILES: { html: true } }). Aggiornare DOMPurify spesso: le versioni nuove chiudono aggiramenti.- Trusted Types (
require-trusted-types-for 'script'; trusted-types dompurify) fa rifiutare al browser le stringhe passate ainnerHTMLe simili che non arrivano da una policy dichiarata. È supportato almeno dai browser basati su Chromium; dove manca la direttiva viene ignorata. Provarlo prima inReport-Only: le librerie che usanoinnerHTMLcompaiono nei rapporti. DOMPurify conRETURN_TRUSTED_TYPE: trueusa la sua policydompurify. Attenzione: MapLibre crea il suo worker da un URLblob:, che per Trusted Types è un punto da proteggere: tenere TT inReport-Onlyoppure aggiungere una policydefaultstretta che accetta solo URLblob:.
6. Token e sessioni nel browser
localStorage(default di Supabase JS, chiavesb-<ref>-auth-token): qualunque script nella pagina legge access e refresh token e li usa altrove. CookieHttpOnly: lo script non li legge, ma un XSS agisce come l'utente finché la pagina è aperta, e servono un server o una funzione sul proprio dominio che li impostino. In entrambi i casi la difesa vera è l'assenza di XSS.- Con Supabase:
createClient(url, key, { auth: { flowType: 'pkce' } })(il codice nell'URL non serve senza il verificatore salvato nel browser; nel flusso implicito i token arrivano nell'URL), rilevamento dei refresh token riusati attivo, durata e inattività delle sessioni limitate dove il piano lo permette, "esci da tutti i dispositivi" consignOut({ scope: 'global' }). - Più account salvati per il cambio rapido moltiplicano il danno: un XSS li prende tutti. Tenerne pochi, togliere quelli non usati da settimane, richiedere l'accesso dopo un periodo. Cifrarli nel browser non serve: lo script dell'attaccante gira nella stessa origine e decifra.
- All'uscita cancellare tutto ciò che resta sul dispositivo: sessione, cache delle query salvate, IndexedDB dei messaggi, Cache Storage. Mai token negli URL, nei log, negli eventi di analytics o negli errori inviati ai servizi di monitoraggio.
7. Il bundle è pubblico
- Tutto ciò che entra nel build si legge: variabili
VITE_*(eNEXT_PUBLIC_*,PUBLIC_*,VUE_APP_*), costanti, rotte "admin". Nel frontend solo chiavi pubblicabili: Supabasesb_publishable_…, chiavi di mappe limitate al dominio nella console del fornitore. Maisb_secret_…/service_role, chiavi Stripesk_…, token di servizi email o di AI: vanno in Edge Functions (api-sicura). - Mai
define: { 'process.env': process.env }invite.confignéenvPrefixallargati: mettono nel bundle tutto l'ambiente di build. Nascondere una pagina nel client non è un controllo d'accesso: il permesso si verifica nel database o nella funzione. - Source map:
build.sourcemap: false, oppure'hidden'per inviarle al servizio di errori e poi cancellarle dadist/:hiddentoglie solo il riferimento, i file restano scaricabili. - Un segreto pubblicato anche una volta si ruota subito (risposta-incidenti): restano copie in cache e negli archivi. Chi entra nell'account dell'hosting o di npm pubblica il JS che vuole: 2FA ovunque (account-fornitori).
8. Script di terzi, SRI, analytics
- Ogni script di terzi ha i poteri del vostro codice, token compresi. Preferire pacchetti npm inclusi nel build a script da CDN; font serviti da sé (ogni richiesta a un CDN di font porta l'IP dell'utente al fornitore). Captcha: solo il dominio del fornitore (Turnstile:
https://challenges.cloudflare.cominscript-srceframe-src). - Uno script esterno inevitabile ha versione fissa e hash:
<script src="https://cdn.example.com/lib@1.2.3/lib.min.js" integrity="sha384-…" crossorigin="anonymous">, con l'hash daopenssl dgst -sha384 -binary lib.min.js | openssl base64 -A. Gli script che cambiano da soli (loader di analytics, widget di chat, tag manager) non possono avere SRI: fuori dalle pagine con sessione, o niente. - Analytics senza cookie e senza dati personali dove possibile, server nell'UE o proprio; consenso per ciò che non è indispensabile (direttiva ePrivacy, art. 5(3)). Togliere query e frammento dagli URL inviati (codici di invito,
?code=), niente ID utente in chiaro, posizioni o dati dell'art. 9 GDPR (privacy-posizione).
9. Redirect, postMessage, clickjacking
?next=e simili: solo percorsi della propria origine, controllati con il parser (//evil.come/\evil.comsembrano relativi ma non lo sono). IredirectTodi Supabase Auth vanno anche negli URL consentiti (supabase-sicuro).
export function nextSicuro(raw, fallback = '/') {
try { const u = new URL(raw ?? fallback, location.origin);
return u.origin === location.origin ? u.pathname + u.search + u.hash : fallback; }
catch { return fallback; }
}postMessage: chi riceve confrontaevent.origincon un valore esatto e controlla la forma dei dati; chi invia indica l'origine di destinazione, mai'*'con dati privati.- Clickjacking:
frame-ancestors 'none'piùX-Frame-Options: DENY; se l'app va incorporata, solo domini esatti. Le azioni gravi (cancellare l'account, condividere la posizione, SOS) chiedono comunque una conferma.
10. Service worker e PWA
- Il service worker intercetta le richieste del suo ambito e resta installato dopo la chiusura: un errore (risposte private in cache, file vecchi serviti per sempre) sopravvive al rilascio della correzione. Si registra solo da un file della propria origine: niente file degli utenti sul dominio dell'app, niente header
Service-Worker-Allowedlargo. - Precache solo dei file del build con hash, mai
.map; in cache solo risposte 200. Mai risposte autenticate: API di Supabase (/rest/v1,/auth/v1, URL firmati di/storage/v1) e richieste conAuthorization. Convite-plugin-pwa: nienteruntimeCachingsu quei domini,injectRegister: 'script'(file esterno, non'inline'). - Tenere pronto un worker che si disattiva da solo, da pubblicare se uno difettoso è in giro:
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (e) => e.waitUntil(caches.keys()
.then((ks) => Promise.all(ks.map((k) => caches.delete(k)))).then(() => self.registration.unregister())));11. Dipendenze del frontend
- Il codice delle dipendenze entra nel bundle e gira con la sessione dell'utente. A settembre 2025 versioni compromesse di pacchetti molto usati (fra cui
chalkedebug) sono rimaste pubblicate per alcune ore con codice pensato per il browser, che dirottava pagamenti in criptovalute. Nello stesso mese il worm "Shai-Hulud" si è propagato da pacchetto a pacchetto rubando i token npm e GitHub dalle macchine degli sviluppatori:npm ci --ignore-scriptsdove possibile, token npm con scadenza e solo dove servono, qualche giorno di attesa prima di adottare una versione appena pubblicata. - Lockfile nella repo;
npm cinella CI e nel comando di build dell'hosting (npm ci && npm run build), mainpm install.npm audit --omit=dev --audit-level=highnella CI. Le dipendenze di sviluppo non entrano nel bundle ma girano sulla macchina di build, con i suoi segreti: si aggiornano anche quelle. - Prima di aggiungere un pacchetto: rilasci recenti, manutentori noti, repository non archiviato, nessun avviso
deprecated. Per piccole funzioni le API del browser (URL,Intl,crypto.randomUUID(),structuredClone). Una dipendenza indiretta vulnerabile si forza conoverrides(npm ls <pacchetto>dice chi la porta). Automazione: sicurezza-github.
Checklist
Controllo prima del rilascio (ogni comando di ricerca deve restituire niente, salvo l'ultimo grep, da rivedere a mano):
npm ci && npm run build && npm audit --omit=dev --audit-level=high
find dist -name '*.map'
grep -rEn 'sb_secret_|sk_live_|rk_live_|BEGIN [A-Z ]*PRIVATE KEY|AKIA[0-9A-Z]{16}' dist
grep -rhoE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+' dist | sort -u | while read -r t; do
echo "${t#*.}" | tr '_-' '/+' | base64 -d 2>/dev/null; echo; done | grep service_role
grep -nP '<script(?![^>]*\bsrc=)' dist/*.html
curl -sI https://app.example.com/percorso/profondo | grep -icE 'content-security-policy|strict-transport|x-frame|nosniff|referrer-policy|permissions-policy' # 6
curl -s https://app.example.com/assets/index-HASH.js.map | head -c 15 # non deve iniziare con {"version":3 (gli host SPA rispondono 200 con index.html ai file mancanti)
grep -rnE 'dangerouslySetInnerHTML|innerHTML|insertAdjacentHTML|setHTML\(|v-html|\{@html' srcPoi: console del browser senza violazioni CSP sulle pagine principali; Mozilla HTTP Observatory e CSP Evaluator di Google senza avvisi gravi; script di terzi, next, postMessage e service worker ricontrollati se toccati.
Limiti da comunicare
- La CSP riduce i danni di un XSS, non lo elimina: un'iniezione che usa solo codice già presente (un
hrefnon filtrato) passa comunque. Sink puliti e CSP servono insieme. - Nel browser non c'è modo di proteggere i token in
localStorageda uno script che gira nella pagina; i cookieHttpOnlyrichiedono un server sul proprio dominio, e anche con quelli un XSS agisce come l'utente. - Il frontend si legge e si modifica: ogni controllo nel client è comodità, la sicurezza dei dati sta nel backend (supabase-sicuro, api-sicura).
- Trusted Types e
report-tonon sono supportati da tutti i browser: sono un livello in più. Gli header dipendono dall'hosting: ricontrollarli concurl -Idopo ogni cambio di piattaforma o di configurazione.
Secure Front-end and SPAs (English)
In a single-page app on static hosting (React, Vite, Vue, Svelte) there is no app server: all the code runs in the browser, anyone can read it, and the data is protected only by the backend rules (supabase-sicuro, api-sicura). The front-end has its own job: never let someone else's code run in the page. With Supabase JS the session lives in localStorage, so an XSS is an account takeover: the attacker's script reads the refresh token and uses it from another computer. General headers and HTTPS are in hardening-siti; this skill covers what is specific to static front-ends.
Mandatory rules
1. A strict CSP, no inline scripts
- On static hosting nonces are impossible (each response needs a fresh value) and hashes must be recomputed on every build. The simple way: no inline scripts, external files only and
script-src 'self'. The script that applies the theme before first paint goes in a file (/theme-init.js) loaded in<head>. - Never
'unsafe-inline'or'unsafe-eval'inscript-src: a library that needsevalgets replaced or used precompiled. WebAssembly only needs'wasm-unsafe-eval'.'unsafe-inline'instyle-srcis acceptable only if a library injects<style>tags; React styles (style={{…}}) work anyway. connect-srcwith exact domains, neverhttps:or*.supabase.co: the latter lets stolen data be sent to the attacker's own Supabase project. Maps load tiles, styles and glyphs withfetch(soconnect-src) and often start workers fromblob:.img-srconly from your own domains: an image whose URL a user chose makes every viewer call the attacker's server, which gets their IP (that is, their area). Example for React + Supabase + a map:
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob: https://REF.supabase.co;
connect-src 'self' https://REF.supabase.co wss://REF.supabase.co https://tiles.openfreemap.org;
worker-src 'self' blob:; font-src 'self'; manifest-src 'self'; frame-src 'none'; object-src 'none';
base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; report-to cspreport-to cspneeds the headerReporting-Endpoints: csp="https://…"(a collection service or a rate-limited Edge Function); addreport-urifor browsers withoutreport-to. Ship a new policy asContent-Security-Policy-Report-Onlyfor a week first, reading the reports. Locally, the same CSP inpreview.headersofvite.configshows the errors undervite preview. The<meta http-equiv>tag ignoresframe-ancestors,report-uriandsandbox: only where the host allows no headers.
2. Headers on static hosting
- Headers live in the platform's file, versioned with the code, and must also apply to the SPA rewrite (
/*→/index.html). Render (render.yaml):
headers:
- path: /*
name: Content-Security-Policy
value: "default-src 'self'; script-src 'self'; …"- Netlify:
public/_headerswith a/*line and, indented below it,Content-Security-Policy: …. Vercel:vercel.jsonwith{"headers":[{"source":"/(.*)","headers":[{"key":"Content-Security-Policy","value":"…"}]}]}. index.htmlwithCache-Control: no-cache, so a fix reaches everyone on the next load; hashed files in/assets/*withpublic, max-age=31536000, immutable.- Alongside the CSP:
X-Frame-Options: DENY,Strict-Transport-Security,X-Content-Type-Options: nosniff,Referrer-Policyand aPermissions-Policywith only what the app uses (geolocation=(self)for the friends map,camera=(self)for stories, the rest()) (hardening-siti).
3. Injection points (sinks)
- React escapes text in JSX. The exceptions must be searched for in every review:
dangerouslySetInnerHTML,ref.current.innerHTML,insertAdjacentHTML,document.write,eval/new Function/setTimeout("…"),location.href = …; in other frameworksv-html,{@html},bypassSecurityTrust…. The ESLint rulesreact/no-dangerandno-restricted-properties(oninnerHTML,outerHTML) flag new ones in CI. - Non-React libraries write HTML: map popups (
popup.setHTML()in MapLibre and Mapbox,bindPopup("…")in Leaflet), chart tooltips whose formatters return HTML, toasts. Names of tables, events, groups and profiles go in as text:
const el = document.createElement('div');
el.textContent = event.title; // text, never HTML
new maplibregl.Popup().setDOMContent(el).setLngLat(pos).addTo(map);- If a library only accepts HTML, the text goes through one shared escape function, never hand-made concatenation:
export const esc = (s) => String(s ?? '').replace(/[&<>"']/g,
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);4. User-supplied URLs and CSS
- An
hrefchosen by a user (profile website, chat link) can bejavascript:. Don't rely on the framework: check the scheme with the URL parser and accept onlyhttps:andmailto:. For URLs, never regexes orstartsWith('http'). The same forsrc,window.openandlocation.assign.
export function safeUrl(raw) {
try { const u = new URL(raw); return ['https:', 'mailto:'].includes(u.protocol) ? u.href : null; }
catch { return null; }
}- External links with
target="_blank" rel="noopener noreferrer"(user content: alsonofollow ugc). User images, video and audio only from your own Storage, never free external URLs. Iframes with user URLs: never. - Never free user CSS in
styleor a<style>tag: it can cover the UI with a fake sign-in form or read attributes through selectors. Colours and themes from a list or a checked format (/^#[0-9a-f]{6}$/i).
5. Markdown, user HTML and Trusted Types
react-markdownwithoutrehype-rawdoes not render raw HTML; if you need it, addrehype-sanitize. With libraries that produce a string (marked,markdown-it), sanitize with DOMPurify when displaying, not only on save, because the data may have been written by calling the API directly:DOMPurify.sanitize(marked.parse(text), { USE_PROFILES: { html: true } }). Update DOMPurify often: new releases close bypasses.- Trusted Types (
require-trusted-types-for 'script'; trusted-types dompurify) makes the browser reject strings passed toinnerHTMLand similar sinks unless they come from a declared policy. It is supported at least by Chromium-based browsers; where missing, the directive is ignored. Try it inReport-Onlyfirst: libraries usinginnerHTMLshow up in the reports. DOMPurify withRETURN_TRUSTED_TYPE: trueuses its owndompurifypolicy. Beware: MapLibre creates its worker from ablob:URL, a Trusted Types sink: keep TT inReport-Onlyor add a narrowdefaultpolicy that only acceptsblob:URLs.
6. Tokens and sessions in the browser
localStorage(the Supabase JS default, keysb-<ref>-auth-token): any script in the page reads the access and refresh tokens and uses them elsewhere.HttpOnlycookies: scripts cannot read them, but an XSS acts as the user while the page is open, and they need a server or function on your own domain to set them. Either way, the real defense is having no XSS.- With Supabase:
createClient(url, key, { auth: { flowType: 'pkce' } })(the code in the URL is useless without the verifier stored in the browser; in the implicit flow the tokens arrive in the URL), detection of reused refresh tokens on, session lifetime and inactivity limits where the plan allows, "sign out everywhere" withsignOut({ scope: 'global' }). - Several saved accounts for quick switching multiply the damage: one XSS takes them all. Keep few, drop those unused for weeks, require signing in again after a period. Encrypting them in the browser doesn't help: the attacker's script runs in the same origin and decrypts.
- On sign-out, delete everything left on the device: session, persisted query cache, message IndexedDB, Cache Storage. Never tokens in URLs, logs, analytics events or errors sent to monitoring services.
7. The bundle is public
- Everything in the build can be read:
VITE_*variables (andNEXT_PUBLIC_*,PUBLIC_*,VUE_APP_*), constants, "admin" routes. Only publishable keys in the front-end: Supabasesb_publishable_…, map keys restricted to your domain in the provider's console. Neversb_secret_…/service_role, Stripesk_…keys, email or AI service tokens: they belong in Edge Functions (api-sicura). - Never
define: { 'process.env': process.env }invite.confignor a widenedenvPrefix: they put the whole build environment in the bundle. Hiding a page in the client is not access control: the permission is checked in the database or the function. - Source maps:
build.sourcemap: false, or'hidden'to upload them to the error service and then delete them fromdist/:hiddenonly drops the reference, the files stay downloadable. - A secret published even once is rotated at once (risposta-incidenti): copies remain in caches and archives. Whoever gets into the hosting or npm account ships whatever JS they like: 2FA everywhere (account-fornitori).
8. Third-party scripts, SRI, analytics
- Every third-party script has your code's powers, tokens included. Prefer npm packages bundled in the build over CDN scripts; self-host fonts (every request to a font CDN hands the user's IP to the provider). Captchas: only the provider's domain (Turnstile:
https://challenges.cloudflare.cominscript-srcandframe-src). - An unavoidable external script gets a pinned version and a hash:
<script src="https://cdn.example.com/lib@1.2.3/lib.min.js" integrity="sha384-…" crossorigin="anonymous">, with the hash fromopenssl dgst -sha384 -binary lib.min.js | openssl base64 -A. Scripts that change on their own (analytics loaders, chat widgets, tag managers) cannot have SRI: keep them off signed-in pages, or drop them. - Analytics without cookies or personal data where possible, servers in the EU or your own; consent for anything not strictly necessary (ePrivacy Directive, art. 5(3)). Strip query and fragment from reported URLs (invite codes,
?code=), no plain user IDs, locations or GDPR art. 9 data (privacy-posizione).
9. Redirects, postMessage, clickjacking
?next=and the like: only paths on your own origin, checked with the parser (//evil.comand/\evil.comlook relative but are not). Supabase AuthredirectTovalues also go in the allowed URLs (supabase-sicuro).
export function safeNext(raw, fallback = '/') {
try { const u = new URL(raw ?? fallback, location.origin);
return u.origin === location.origin ? u.pathname + u.search + u.hash : fallback; }
catch { return fallback; }
}postMessage: the receiver comparesevent.originwith an exact value and checks the data's shape; the sender names the target origin, never'*'with private data.- Clickjacking:
frame-ancestors 'none'plusX-Frame-Options: DENY; if the app must be embedded, exact domains only. Serious actions (deleting the account, sharing location, SOS) still ask for confirmation.
10. Service workers and PWAs
- A service worker intercepts the requests in its scope and stays installed after the page closes: a bug (private responses cached, old files served forever) outlives the release of the fix. It can only be registered from a file on your own origin: no user files on the app's domain, no broad
Service-Worker-Allowedheader. - Precache only hashed build files, never
.map; cache only 200 responses. Never authenticated responses: Supabase APIs (/rest/v1,/auth/v1, signed/storage/v1URLs) and requests withAuthorization. Withvite-plugin-pwa: noruntimeCachingon those domains,injectRegister: 'script'(an external file, not'inline'). - Keep a self-disabling worker ready, to ship if a faulty one is out there:
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (e) => e.waitUntil(caches.keys()
.then((ks) => Promise.all(ks.map((k) => caches.delete(k)))).then(() => self.registration.unregister())));11. Front-end dependencies
- Dependency code ends up in the bundle and runs with the user's session. In September 2025 compromised versions of widely used packages (including
chalkanddebug) stayed published for a few hours with code built for the browser that diverted cryptocurrency payments. The same month the "Shai-Hulud" worm spread from package to package, stealing npm and GitHub tokens from developers' machines:npm ci --ignore-scriptswhere possible, npm tokens that expire and only where needed, a few days' wait before adopting a freshly published version. - Lockfile in the repo;
npm ciin CI and in the host's build command (npm ci && npm run build), nevernpm install.npm audit --omit=dev --audit-level=highin CI. Dev dependencies don't enter the bundle but run on the build machine, with its secrets: keep them updated too. - Before adding a package: recent releases, known maintainers, repository not archived, no
deprecatedwarning. For small tasks, browser APIs (URL,Intl,crypto.randomUUID(),structuredClone). Force a patched transitive dependency withoverrides(npm ls <package>shows who pulls it in). Automation: sicurezza-github.
Checklist
Pre-release check (every search command must return nothing, except the last grep, which you review by hand):
npm ci && npm run build && npm audit --omit=dev --audit-level=high
find dist -name '*.map'
grep -rEn 'sb_secret_|sk_live_|rk_live_|BEGIN [A-Z ]*PRIVATE KEY|AKIA[0-9A-Z]{16}' dist
grep -rhoE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+' dist | sort -u | while read -r t; do
echo "${t#*.}" | tr '_-' '/+' | base64 -d 2>/dev/null; echo; done | grep service_role
grep -nP '<script(?![^>]*\bsrc=)' dist/*.html
curl -sI https://app.example.com/deep/path | grep -icE 'content-security-policy|strict-transport|x-frame|nosniff|referrer-policy|permissions-policy' # 6
curl -s https://app.example.com/assets/index-HASH.js.map | head -c 15 # must not start with {"version":3 (SPA hosts answer missing files with 200 and index.html)
grep -rnE 'dangerouslySetInnerHTML|innerHTML|insertAdjacentHTML|setHTML\(|v-html|\{@html' srcThen: browser console free of CSP violations on the main pages; Mozilla HTTP Observatory and Google's CSP Evaluator without serious warnings; third-party scripts, next, postMessage and the service worker re-checked if touched.
Limits to communicate
- A CSP reduces the damage of an XSS, it does not remove it: an injection that only uses code already present (an unfiltered
href) still works. Clean sinks and a CSP are needed together. - In the browser there is no way to protect tokens in
localStoragefrom a script running in the page;HttpOnlycookies need a server on your own domain, and even with them an XSS acts as the user. - The front-end can be read and modified: every client-side check is convenience, data security lives in the backend (supabase-sicuro, api-sicura).
- Trusted Types and
report-toare not supported by every browser: they are an extra layer. Headers depend on the host: re-check them withcurl -Iafter any platform or configuration change.
In this repository14 other files it gives its agents.
- account-fornitoriSkill
- aggiornamenti-runtimeSkill
- api-sicuraSkill
- architettura-fortezzaSkill
- autenticazione-sicuraSkill
- difesa-attacchiSkill
- email-dominioSkill
- hardening-sitiSkill
- privacy-pagamentiSkill
- privacy-posizioneSkill
- risposta-incidentiSkill
- sicurezza-githubSkill
- sicurezza-socialSkill
- supabase-sicuroSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

