Sécurité frontend en entreprise : le client n'est pas fiable
Sécurité frontend en entreprise : côté serveur, dans l'UI, et , , tokens, aucun secret dans le , audit.

Un bouton de virement est désactivé parce que le plafond journalier est atteint. N'importe qui peut ouvrir et le réactiver, ou se passer du bouton et appeler l'API directement. Si le plafond n'existe que dans le composant, la banque n'a pas de plafond.
C'est toute la première leçon de la sécurité frontend dans une app d'entreprise : le navigateur est la partie que vous ne contrôlez pas. Le frontend peut rendre les attaques plus difficiles. Il ne peut pas être l'endroit où une règle est appliquée.
Les quelques concepts qui comptent
Le contrôle d'accès tourne sur le serveur. L' le dit clairement : le contrôle d'accès « n'est efficace que s'il est mis en œuvre dans du code de confiance côté serveur ou dans des API serverless ». Le contrôle d'accès défaillant garde la première place du Top 10:2025 de l'OWASP, et parmi les faiblesses listées figurent le contournement des contrôles « en modifiant l'URL » et la falsification d'un . La vérification dans le composant, c'est de l'UX. La vérification sur le serveur, c'est la règle.
Le RBAC dans l'UI sert à la clarté, pas à la protection. Masquez le bouton d'approbation pour un utilisateur qui ne peut pas approuver, pour que l'écran ait du sens. Prenez les rôles dans la réponse du serveur, et attendez-vous à ce que le serveur refuse quand même. Un frontend qui décode un token pour décider de ce que l'utilisateur a le droit de faire a déplacé la décision au seul endroit que l'utilisateur peut modifier.
Des guards de route pour les permissions, sur la route parente. Dans , placez un seul sur la route parente d'un espace plutôt qu'un guard sur chaque écran. canActivateChild « détermine si un utilisateur peut accéder aux routes enfants d'une route parente donnée », donc un nouvel écran sous /admin est couvert sans que personne ait à penser à l'ajouter. canMatch décide « si une route peut correspondre lors de la mise en correspondance du chemin », et quand il répond non, Angular essaie les autres routes : un utilisateur sans le rôle d'approbateur peut arriver sur une version en lecture seule du même chemin. Les deux lisent les rôles dans la réponse du serveur, et la documentation d'Angular est claire sur la limite : « Ne comptez jamais sur des guards côté client comme seule source de contrôle d'accès. »
XSS : gardez le sanitizer actif. Angular « traite toutes les valeurs comme non fiables par défaut » et assainit les valeurs liées au . Les méthodes bypassSecurityTrust* existent ; la documentation dit « soyez prudent », car faire confiance à une valeur malveillante introduit une vulnérabilité. L'équivalent dans React est dangerouslySetInnerHTML, où du HTML non fiable signifie que vous « risquez d'introduire une vulnérabilité XSS ». En revue, traitez les deux comme un défaut tant que personne n'a prouvé que l'entrée est fiable.
La CSP comme deuxième couche. L'OWASP voit dans une Content Security Policy solide « une deuxième couche de protection efficace », et précise qu'elle « ne doit pas être le seul mécanisme de défense contre les XSS ». Angular prend en charge les , et une option autoCsp pour l'hébergement statique. Pour voir ce que votre site envoie aujourd'hui, passez-le dans l'HTTP Observatory de Mozilla. L'outil « réalise une évaluation approfondie des en-têtes HTTP d'un site et d'autres réglages de sécurité clés ».
Image : tt-tech.io, adapté de web.dev, Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP), CC BY 4.0.
Tokens et sessions. L'OWASP : « Ne stockez pas les identifiants de session dans le local storage, car ces données sont toujours accessibles par JavaScript » ; les cookies « peuvent réduire ce risque grâce à l'attribut httpOnly ». Les cookies ramènent le . Le HttpClient d'Angular lit un cookie XSRF-TOKEN et le renvoie dans un en-tête X-XSRF-TOKEN, et la documentation est claire : cette attaque « doit être traitée d'abord côté serveur ».
Aucun secret dans le bundle, aucun appel direct à un tiers. Tout ce qui part vers le navigateur est public. Une clé d'API dans le code client est une clé d'API publiée. Le backend garde la clé et appelle le service externe.
Auditer sans fuite. Les secteurs réglementés veulent savoir qui a fait quoi. Le frontend aide avec un identifiant d'interaction sur chaque requête, et reste prudent sur ce qu'il journalise : l'OWASP cite les , les tokens d'accès, les mots de passe, les données personnelles sensibles et « les données de compte bancaire ou de titulaire de carte de paiement » parmi les données qui ne devraient en général pas être journalisées telles quelles, seulement masquées, hachées ou retirées.
La vérification dans le composant est pour l'utilisateur. La vérification sur le serveur est pour la banque.
Quand choisir quoi
- Cookie ou token en mémoire : une session par cookie via un (BFF) quand vous maîtrisez les deux côtés ; un token de courte durée en mémoire quand l'API est partagée avec d'autres clients. Jamais le pour les identifiants de session.
- CSP avec nonces ou hashes : des nonces quand un serveur rend chaque réponse ; des hashes
autoCspquand l'app est un ensemble de fichiers statiques sur un . - Masquer ou désactiver : masquez les actions qu'un rôle n'a jamais ; désactivez, en affichant la raison, celles que l'utilisateur pourrait avoir plus tard (un plafond, une signature manquante).
Sur Atlas Bank
Le fichier de règles d'Atlas Bank dit : « Chaque règle métier est appliquée à la frontière. » Les règles de virement existent une seule fois, sous forme de fonction pure dans libs/domain/src/lib/transfer-rules.ts. Le nouvel écran de virement du module 5 (libs/feature/transfers/src/lib/new-transfer.ts sur la branche track1/m5-security-solution) appelle canTransfer() dans un computed pour afficher la raison. La frontière l'appelle de nouveau avant que l'argent ne bouge, dans libs/data-access/banking/src/lib/fake-backend.ts :
// La règle est appliquée à la frontière. Le client fait la même vérification pour l'UX, le serveur la fait pour de vrai.
const verdict = canTransfer({ from, toRib, amountMad, transferredTodayMad: this.transferredToday(fromAccountId) });
if (!verdict.ok) throw new Error(blockerMessage[verdict.reason]);La couche bon marché est mécanique. eslint.config.mjs échoue sur les appels bypassSecurityTrust* et sur un fetch vers une URL absolue. npm run scan:secrets échoue sur les chaînes qui ressemblent aux formats de clés courants dans apps/ et libs/. Les deux tournent dans npm run lint:arch. La carte OWASP du frontend du dépôt associe chaque famille de risques à ce qu'une équipe frontend contrôle, et reste honnête sur les manques : la CSP, la gestion des tokens et la journalisation ne sont pas dans la démo. Il n'y a pas non plus de guards de route pour l'instant.
Ce que j'ai laissé de côté, et pourquoi
- , , . Des risques réels, surtout une affaire d'en-têtes et de configuration. Une ligne chacun dans une checklist de déploiement.
- Les flux et . Le flux se choisit avec l'équipe chargée de l'identité ; la part du frontend, c'est l'endroit où vit le token, traité plus haut.
- La cryptographie dans le navigateur. Ce n'est pas le travail du frontend dans une banque.
Dès lundi
- Cherchez
bypassSecurityTrustetdangerouslySetInnerHTMLdans votre base de code. Chaque occurrence reçoit un responsable et une raison, ou disparaît. - Prenez une règle métier que l'UI vérifie (un plafond, un rôle) et trouvez la ligne côté serveur qui la vérifie aussi. Si vous ne la trouvez pas, c'est un défaut à remonter.
- Ouvrez votre configuration de journalisation et cherchez des tokens ou des données personnelles dans ce que le client envoie.
Pour votre agent de code
Les règles de cet article, écrites pour un agent. Collez-les dans votre AGENTS.md, enregistrez-les comme , ou indiquez le fichier à votre agent.
# Frontend security rules for enterprise apps Source: https://tt-tech.io/blog/frontend-security-enterprise-untrusted-client For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ. ## Rules 1. The browser is untrusted. Every access rule and business limit is enforced on the server. A check in a component is only for the user's screen. 2. When you add a client-side check (a limit, a role, a permission), add or find the same check on the server in the same change. If there is none, report it as a finding. 3. Take roles and permissions from the server's answer. Never decide what the user may do by decoding a token in the client. 4. Route guards are for navigation, not protection. Put `canActivateChild` on the parent route of an area, use `canMatch` for role-based variants of a path, and still expect the server to refuse. 5. Hide actions a role never has. Disable, with the reason shown, actions the user could have later (a limit, a missing signature). 6. Keep the sanitizer on. Never call `bypassSecurityTrust*` (Angular) or use `dangerouslySetInnerHTML` (React) on data from a user or an API. If some markup is wanted, map an allowed subset to elements in the template. 7. Never store session ids or tokens in `localStorage` or `sessionStorage`. Use an `httpOnly` cookie session through a backend for frontend, or a short-lived token in memory when the API is shared with other clients. 8. With cookie sessions, keep CSRF protection on: in Angular, `HttpClient` sends the `XSRF-TOKEN` cookie back as an `X-XSRF-TOKEN` header, and the server must check it. 9. No secrets in client code: no API key, token or password in the frontend, not even temporarily. Everything shipped to the browser is public. 10. The browser never calls a third-party service directly. The backend holds the key and makes the call. 11. Treat the Content Security Policy as a second layer: nonces when a server renders each response, `autoCsp` hashes when the app is static files. Do not loosen it to make a script run. 12. Never log tokens, session ids, passwords, personal data or bank and card data from the client. Mask them or leave them out. Send an interaction id with each request instead, so server logs can be matched. ## Before you say done - Search the diff for `bypassSecurityTrust`, `dangerouslySetInnerHTML`, `localStorage` and strings that look like keys. Remove each hit or explain it in the pull request. - For every client-side rule in the diff, name the server code that enforces it. - List what you checked and what you could not check.
Sources
- OWASP, A01 Broken Access Control - OWASP Top 10:2025, 2025
- Angular, Control route access with guards
- Angular, Security
- React, Common components (e.g. <div>)
- OWASP, Content Security Policy Cheat Sheet
- MDN, HTTP Observatory
- OWASP, HTML5 Security Cheat Sheet
- OWASP, Logging Cheat Sheet
- web.dev, Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)