Retour au blog
28 juillet 2026

Notre propre application stockait les jetons d'authentification dans localStorage. Voici ce que nous avons changé.

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

En juin, nous avons pointé Penetrify sur notre propre application, comme nous le faisons avant une release importante. Le scan est revenu avec un problème dans notre flux de connexion Cognito, et en tirant sur ce fil, cela est devenu quelque chose que je n'ai pas aimé lire : le tableau de bord Penetrify gardait les jetons d'accès et de rafraîchissement de Cognito dans localStorage, en clair, lisibles par n'importe quel JavaScript exécuté sur la page.

Nous ne nous sommes pas assis pour décider cela. C'est le comportement par défaut de la bibliothèque d'authentification que nous avons utilisée, et nous ne l'avons jamais remis en question. C'est précisément la partie qui mérite d'être écrite.

Ce qu'était réellement la découverte

Notre tableau de bord est une application single-page. À la connexion, elle recevait les jetons d'AWS Cognito et le SDK les plaçait dans le stockage du navigateur, car c'est là que les SDK les placent quand aucun serveur ne participe à la session. N'importe quel script ayant accès à la page pouvait les lire.

Cela signifie qu'un seul bug de cross-site scripting n'importe où dans l'application, une seule extension de navigateur malveillante ou une seule dépendance npm compromise, et un attaquant repart avec les deux jetons. Le jeton d'accès le fait entrer dans le compte. Le jeton de rafraîchissement l'y maintient, car il reste valide longtemps après que la session semble fermée, jusqu'à ce que quelque chose le révoque explicitement.

Pour nos utilisateurs, la portée incluait les informations de facturation, les résultats de scan de leurs applications et les données de versements d'affiliation. Autant que nous puissions en juger, personne ne l'a exploité. Ce n'est pas le sujet. C'était atteignable, et c'était notre propre valeur par défaut à corriger.

Pourquoi chiffrer localStorage n'est pas la solution

Le premier réflexe est de chiffrer les jetons avant de les stocker. Cela n'aide pas. La clé de déchiffrement doit vivre dans le même JavaScript que l'attaquant exécute déjà. Vous avez ajouté une étape, pas une frontière.

Le second réflexe est de déplacer les jetons dans un cookie côté client. Cela n'aide pas non plus. Un cookie posé par JavaScript est lisible par JavaScript. La propriété qui compte est HttpOnly, et seul un serveur peut la définir, via un en-tête Set-Cookie. Si votre correctif tourne dans le navigateur, ce n'est pas un correctif.

Ce que nous avons livré à la place

Nous avons déplacé toute la session vers des cookies posés par le serveur, avec un motif token handler dans le backend que nous avions déjà :

  • Le backend FastAPI effectue les appels à Cognito et renvoie la session sous forme de cookies avec HttpOnly, Secure et SameSite. Le navigateur ne voit jamais de jeton.
  • Le middleware d'authentification lit le jeton d'accès depuis le cookie au lieu d'un en-tête Authorization.
  • Comme les cookies sont envoyés automatiquement, les requêtes modifiantes portent désormais un en-tête de jeton CSRF, que le backend vérifie.
  • La déconnexion effectue un global sign-out côté Cognito, pour qu'un jeton ayant fui plus tôt meure avec la session au lieu de lui survivre.
  • Le SDK d'authentification a totalement disparu du navigateur.

Nous n'avions pas besoin d'un nouveau service. Notre application statique et notre API sont déjà de même origine : CloudFront sert l'application et route /api/* vers API Gateway. Un cookie first-party est donc envoyé à chaque appel d'API, sans infrastructure supplémentaire.

Nous avons d'abord regardé la voie recommandée par le fournisseur, qui aurait signifié adopter une interface de connexion hébergée, et nous l'avons rejetée. Elle aurait jeté nos propres écrans de connexion, de MFA et de réinitialisation de mot de passe pour résoudre un problème que nous savons résoudre dans notre backend existant.

Ce que cela a coûté

Environ une semaine de travail, et aucun changement visible pour les utilisateurs. C'est le résumé honnête de la plupart du travail de sécurité, et c'est exactement pourquoi ce genre de chose est reporté indéfiniment dans une startup. Rien ne bouge sur la roadmap. Aucun client ne le demande. La seule chose qui change, c'est ce qu'un unique script injecté peut faire à vos utilisateurs.

Un détail pratique qui a rendu le déploiement sûr : pendant le rollout, le backend acceptait encore l'ancien chemin par en-tête, donc revenir sur le déploiement du frontend était un rollback en une étape si quelque chose tournait mal.

Vérifiez votre propre application. Cela prend environ 30 secondes.

Ouvrez votre application, connectez-vous, puis ouvrez les DevTools :

  • Onglet Application, Local Storage. Cherchez de longues valeurs contenant deux points. Cette forme est un JWT.
  • Vérifiez Session Storage dans le même panneau. Même problème, durée de vie plus courte.
  • Dans la console, tapez document.cookie. Tout ce qui apparaît là est par définition lisible par JavaScript : un cookie de session que vous voyez dans cette sortie ne vous protège pas.

Si vous trouvez un jeton, c'est une découverte. Pas théorique, et pas quelque chose qu'un audit de dépendances au vert ou une analyse statique propre vous aurait signalé.

Pourquoi nous publions ceci

Deux raisons, et la seconde concerne notre propre produit.

D'abord, les valeurs par défaut sont des décisions que quelqu'un d'autre a prises pour vous. Elles arrivent avec une bibliothèque, elles sont pratiques, et elles ne sont presque jamais un modèle de menace. La nôtre était un défaut raisonnable pour un tutoriel et mauvais pour une application qui détient des données de paiement.

Ensuite, un scan est une indication, pas un verdict. Le nôtre a signalé le flux de connexion et prouvé qu'il était atteignable. Il ne nous a pas livré la phrase « votre bibliothèque d'authentification utilise localStorage par défaut, et c'est cela qu'il faut changer ». Passer de l'alerte au correctif a voulu dire ouvrir notre propre chemin d'authentification et le lire. C'est aussi ainsi que nous attendons que vous utilisiez nos rapports : la découverte vous dit où regarder et montre la requête qui le prouve, puis vous décidez quel est le bon correctif dans votre architecture.

Donc : lancez le scan sur votre propre application, puis lisez le code qu'il désigne. Nous avons fait les deux, et aucune des deux moitiés n'aurait suffi seule.

Si vous voulez le regard extérieur sur votre application tout de suite, notre contrôle de sécurité gratuit prend environ 60 secondes et ne demande aucune inscription. Si vous voulez le rapport complet avec preuves et correctifs, le premier scan coûte $29 et est crédité sur votre plan. Et si vous préférez la chose gratuite que nous venons de décrire, ouvrez les DevTools et allez regarder votre propre local storage. Celle-là ne coûte rien.

Frequently Asked Questions

Quels types de vulnérabilités Penetrify détecte-t-il ?

Penetrify détecte toutes les catégories de vulnérabilités OWASP Top 10, notamment les injections SQL, XSS, CSRF, IDOR, les failles d'authentification, les mauvaises configurations de sécurité et l'exposition de données sensibles. Il teste également la sécurité des API, la gestion des sessions et les mauvaises configurations courantes dans Supabase, Firebase et Bubble.

Combien de temps dure un test de pénétration IA ?

Un scan rapide se termine en 15–30 minutes. Un scan standard dure 1–2 heures avec une couverture plus large. Un scan approfondi peut durer plusieurs heures pour les applications complexes.

Que contient un rapport Penetrify ?

Chaque rapport comprend un résumé exécutif, un score de sécurité global, des résultats classés par gravité (Critique, Élevé, Moyen, Faible), des étapes de reproduction détaillées et des recommandations de remédiation concrètes rédigées pour les développeurs – pas pour les responsables conformité.

Related articles

Alternatives DAST en 2026 : Quand l'analyse dynamique ne suffit pas (et ce qu'il faut utiliser à la place)
Les scanners DAST manquent les flux d'authentification, la logique métier et les API modernes. Voici une comparaison honnête entre DAST, SAST, IAST, PTaaS et le Penetration Testing autonome par IA — et quand utiliser chacun.
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