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,SecureeSameSite. 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.
