Torna al Blog
28 luglio 2026

La nostra app teneva i token di autenticazione in localStorage. Ecco cosa abbiamo cambiato.

Viktor Bulanek
Founder & CTO, Penetrify
MSc IT Security · 20+ years in security · 4x Ex-CTO

A giugno abbiamo puntato Penetrify sulla nostra applicazione, come facciamo prima di una release importante. La scansione è tornata con un problema nel nostro flusso di login con Cognito, e tirando quel filo è diventata qualcosa che non mi ha fatto piacere leggere: la dashboard di Penetrify teneva i token di accesso e di refresh di Cognito in localStorage, in chiaro, leggibili da qualsiasi JavaScript in esecuzione sulla pagina.

Non ci siamo seduti a deciderlo. È il default della libreria di autenticazione che abbiamo usato, e non l'abbiamo mai messo in dubbio. È esattamente la parte su cui vale la pena scrivere.

Cos'era davvero il finding

La nostra dashboard è una single-page app. Al login riceveva i token da AWS Cognito e l'SDK li metteva nello storage del browser, che è dove gli SDK li mettono quando nessun server partecipa alla sessione. Qualsiasi script con accesso alla pagina poteva leggerli.

Significa che un solo bug di cross-site scripting in qualunque punto dell'app, una sola estensione malevola del browser o una sola dipendenza npm compromessa, e un attaccante se ne va con entrambi i token. Il token di accesso lo porta dentro l'account. Quello di refresh lo tiene dentro, perché resta valido a lungo dopo che la sessione sembra chiusa, finché qualcosa non lo revoca esplicitamente.

Per i nostri utenti il raggio d'azione includeva i dati di fatturazione, i risultati delle scansioni delle loro applicazioni e i dati sui pagamenti di affiliazione. Per quanto possiamo giudicare, nessuno l'ha sfruttato. Non è il punto. Era raggiungibile, ed era un nostro default da sistemare.

Perché cifrare localStorage non è la soluzione

Il primo istinto è cifrare i token prima di salvarli. Non serve. La chiave di decifratura deve stare nello stesso JavaScript che l'attaccante sta già eseguendo. Hai aggiunto un passaggio, non un confine.

Il secondo istinto è spostare i token in un cookie dal lato client. Non serve nemmeno questo. Un cookie impostato da JavaScript è leggibile da JavaScript. La proprietà che conta è HttpOnly, e solo un server può impostarla, tramite un header Set-Cookie. Se la tua correzione gira nel browser, non è una correzione.

Cosa abbiamo rilasciato al suo posto

Abbiamo spostato l'intera sessione su cookie impostati dal server, con un pattern token handler dentro il backend che avevamo già:

  • Il backend FastAPI esegue le chiamate a Cognito e restituisce la sessione come cookie con HttpOnly, Secure e SameSite. Il browser non vede mai un token.
  • Il middleware di autenticazione legge il token di accesso dal cookie invece che da un header Authorization.
  • Poiché i cookie vengono inviati automaticamente, le richieste che modificano stato portano ora un header con token CSRF, che il backend verifica.
  • Il logout esegue un global sign-out sul lato Cognito, così un token trafugato in passato muore con la sessione invece di sopravviverle.
  • L'SDK di autenticazione è uscito completamente dal browser.

Non ci serviva un nuovo servizio. La nostra app statica e la nostra API sono già same-origin: CloudFront serve l'app e instrada /api/* verso API Gateway. Un cookie first-party viene quindi inviato a ogni chiamata API senza infrastruttura aggiuntiva.

Abbiamo prima valutato la strada consigliata dal fornitore, che avrebbe significato adottare una UI di login ospitata, e l'abbiamo scartata. Avrebbe buttato le nostre schermate di login, MFA e reset password per risolvere un problema che sappiamo risolvere nel backend che abbiamo già.

Quanto è costato

Circa una settimana di lavoro, e nessun cambiamento visibile per gli utenti. È il riassunto onesto di quasi tutto il lavoro sulla sicurezza, ed è esattamente il motivo per cui in una startup queste cose si rinviano all'infinito. Sulla roadmap non si muove niente. Nessun cliente lo chiede. L'unica cosa che cambia è cosa può fare ai tuoi utenti un singolo script iniettato.

Un dettaglio pratico che ha reso sicuro il rilascio: durante il rollout il backend accettava ancora la vecchia via tramite header, quindi tornare indietro sul deploy del frontend era un rollback in un solo passo se qualcosa andava storto.

Controlla la tua app. Ci vogliono circa 30 secondi.

Apri la tua app, fai login, poi apri i DevTools:

  • Tab Application, Local Storage. Cerca valori lunghi con due punti dentro. Quella forma è un JWT.
  • Controlla Session Storage nello stesso pannello. Stesso problema, vita più breve.
  • In console digita document.cookie. Tutto ciò che compare lì è per definizione leggibile da JavaScript, quindi un cookie di sessione che vedi in quell'output non ti sta proteggendo.

Se trovi un token, quello è un finding. Non teorico, e non qualcosa che ti avrebbe segnalato un audit delle dipendenze tutto verde o un'analisi statica pulita.

Perché lo pubblichiamo

Due ragioni, e la seconda riguarda il nostro prodotto.

Primo: i default sono decisioni che qualcun altro ha preso per te. Arrivano con una libreria, sono comodi, e non sono quasi mai un modello di minaccia. Il nostro era un default ragionevole per un tutorial e sbagliato per un'app che tiene dati di pagamento.

Secondo: una scansione è un'indicazione, non un verdetto. La nostra ha segnalato il flusso di login e ha dimostrato che era raggiungibile. Non ci ha consegnato la frase «la tua libreria di autenticazione usa localStorage per default, ed è quello da cambiare». Passare dalla segnalazione alla correzione ha significato aprire il nostro percorso di autenticazione e leggerlo. È anche così che ci aspettiamo che tu usi i nostri report: il finding ti dice dove guardare e mostra la richiesta che lo prova, poi decidi tu qual è la correzione giusta nella tua architettura.

Quindi: lancia la scansione sulla tua applicazione e poi leggi il codice che indica. Abbiamo fatto entrambe le cose, e nessuna delle due metà da sola sarebbe bastata.

Se vuoi subito lo sguardo dall'esterno sulla tua app, il nostro controllo di sicurezza gratuito richiede circa 60 secondi e non chiede registrazione. Se vuoi il report completo con prove e correzioni, la prima scansione costa $29 e viene accreditata sul tuo piano. E se preferisci la cosa gratis che abbiamo appena descritto, apri i DevTools e guarda nel tuo local storage. Quella non costa niente.

Frequently Asked Questions

Quali tipi di vulnerabilità rileva Penetrify?

Penetrify rileva tutte le categorie di vulnerabilità OWASP Top 10, inclusi SQL injection, XSS, CSRF, IDOR, autenticazione compromessa, configurazioni di sicurezza errate ed esposizione di dati sensibili. Testa anche la sicurezza delle API, la gestione delle sessioni e le comuni configurazioni errate in Supabase, Firebase e Bubble.

Quanto dura un test di penetrazione con IA?

Una scansione rapida si completa in 15–30 minuti. Una scansione standard dura 1–2 ore con una copertura più ampia. Una scansione approfondita può durare diverse ore per applicazioni complesse.

Cosa include un report di Penetrify?

Ogni report include un sommario esecutivo, un punteggio di sicurezza complessivo, i risultati classificati per gravità (Critico, Alto, Medio, Basso), procedure di riproduzione dettagliate e indicazioni concrete di rimediazione scritte per gli sviluppatori, non per i responsabili della conformità.

Related articles

Alternative al DAST nel 2026: Quando la scansione dinamica non basta (e cosa usare invece)
Gli scanner DAST non rilevano i flussi di autenticazione, le logiche di business e le moderne API. Ecco un confronto onesto tra DAST e SAST, IAST, PTaaS, e il Penetration Testing autonomo basato su AI, e quando utilizzare ciascuno di essi.
What an autonomous pentest agent found in 3,847 apps — and what your scanner didn't
A data breakdown of 47,291 exploitation-validated findings, with methodology and limitations. 91% of the SQL injection we found shipped despite a SAST gate in CI; 78% of critical findings needed no login.
Indirect Prompt Injection to Data Exfiltration: When the Model Has Tools
Prompt injection on its own is a curiosity. Prompt injection reaching a tool that holds real credentials is a breach — and the instruction does not have to come from your user. It can arrive inside the document your app was asked to summarise.

Explore more