flawopen.com/xss/Javascript
XSS en JavaScript : sécuriser DOM, React et Express
Préférez les API texte ; assainissez le HTML, validez les URL et appliquez Trusted Types pour protéger DOM/React (CWE-79).
Imaginez un pupitre de régie. Si le régisseur traite chaque billet du public comme une indication scénique, un billet peut ajouter une scène imprévue ; affiché comme une simple réplique, il reste du texte. Dans le navigateur, innerHTML et dangerouslySetInnerHTML de React demandent au DOM d’interpréter le texte fourni comme du balisage, tandis que textContent l’affiche littéralement. Échappez le texte ordinaire, assainissez le peu de HTML autorisé et vérifiez les schémas d’URL avant de créer des liens.
Concepts Clés et Termes
innerHTML vs textContent- Affecter
innerHTML,outerHTMLouinsertAdjacentHTMLfait analyser la chaîne comme du HTML : un<img onerror>qu'elle contient devient un élément actif.textContentetcreateTextNoden'insèrent que du texte ; rien dans la valeur n'est jamais interprété comme du balisage. Template literals in res.send()res.send()d'Express envoie les chaînes entext/htmlpar défaut. Un template literal évalue${req.query.q}avant l'envoi : la réponse contient donc ce que l'utilisateur a mis dans la query string.req.query.qpeut aussi être un tableau ou un objet ; vérifiez son type.dangerouslySetInnerHTML + DOMPurify- React échappe automatiquement le texte dans le JSX ;
dangerouslySetInnerHTMLest l'exception explicite. Faites passer la valeur parDOMPurify.sanitize(), qui l'analyse avec le propre parseur du navigateur et retire scripts, gestionnaires d'événements et URL dangereuses avant que React ne l'insère. javascript: URLs in href- Le JSX échappe la valeur de l'attribut, mais
href="javascript:..."est une URL valide qui exécute du code au clic. Analysez-la avecnew URL()et n'autorisez quehttp:ethttps:; le parseur d'URL retire les espaces et tabulations initiaux que de simples tests de préfixe laissent passer. Trusted Types- Avec la directive CSP
require-trusted-types-for 'script', les navigateurs compatibles refusent les chaînes brutes dans des sinks commeinnerHTMLet n'acceptent que des objetsTrustedHTMLcréés par une politique que vous définissez, généralement une politique qui appelle DOMPurify. Aucun nouveau sink ne passe inaperçu.
Correspondances de référentiels
- Référence CWE pour la faiblesse décrite dans ce guide : CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Déroulement de l'Attaque Étape par Étape
La saisie atteint un sink
Une recherche, un champ de profil chargé depuis une API ou un lien enregistré par un autre utilisateur arrive sous forme de chaîne et est passé à innerHTML, dangerouslySetInnerHTML, href ou res.send().
La valeur devient du balisage
Avec la valeur de test inoffensive <img src=x onerror=alert(1)>, innerHTML et res.send() produisent un vrai élément <img> ; un lien enregistré javascript:alert(1) devient un script cliquable.
Le navigateur l'exécute
L'image ne se charge pas et déclenche onerror, ou l'utilisateur clique sur le lien. Le code s'exécute immédiatement, sans autre action de l'attaquant.
Le script agit au nom du site
Il s'exécute avec l'origine de la page : il peut lire le DOM, appeler les API du site avec les cookies de la victime ou les jetons du localStorage, et modifier ce que voit l'utilisateur.
Code Source : Vulnérable vs Sécurisé
// profile.jsx : chaque fonction ci-dessous laisse une chaîne être analysée comme du HTML
import express from 'express';
const app = express();
// Express : la valeur de la query est placée dans le HTML avant l'envoi
app.get('/search', (req, res) => {
res.send(`<h1>Results for ${req.query.q}</h1>`);
});
// innerHTML analyse name comme du balisage
export function showGreeting(el, name) {
el.innerHTML = `Hello, ${name}`;
}
// React insère le HTML exactement tel qu'il est reçu
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}
// une URL javascript: s'exécute au clic sur le lien
export function Website({ url }) {
return <a href={url}>Website</a>;
}
// profile.jsx : le texte reste du texte ; HTML et URL sont d'abord vérifiés
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';
const app = express();
// seules les chaînes sont acceptées, et elles sont échappées en HTML
app.get('/search', (req, res) => {
const q = typeof req.query.q === 'string' ? req.query.q : '';
res.send(`<h1>Results for ${escapeHtml(q)}</h1>`);
});
// textContent n'analyse jamais de balisage
export function showGreeting(el, name) {
el.textContent = `Hello, ${name}`;
}
// DOMPurify retire scripts et gestionnaires d'événements avant l'insertion
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}
// seules les URL http et https passent ; le reste devient #
function safeHref(url) {
try {
const parsed = new URL(url, window.location.origin);
return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
} catch {
return '#';
}
}
export function Website({ url }) {
return <a href={safeHref(url)}>Website</a>;
}
Liste de Contrôle de Sécurité pour l'Ingénierie
- ✓Activez
react/no-dangereteslint-plugin-no-unsanitizeden CI, et bloquezinnerHTML,outerHTML,insertAdjacentHTMLetdocument.writeavec des valeurs non littérales. - ✓Utilisez
textContentpour le texte ; tout HTML à afficher passe parDOMPurify.sanitize()au moment de l'insertion. - ✓Validez toute URL fournie par l'utilisateur avec
new URL()et une liste autoriséehttp:/https:avant qu'elle n'atteignehref,srcouwindow.location. - ✓Sous Express, rendez via un moteur de vues qui échappe automatiquement ou avec
escape-html, et refusez les paramètres de query qui ne sont pas des chaînes. - ✓Envoyez une CSP avec un
script-srcà nonce, sans'unsafe-inline'et avecrequire-trusted-types-for 'script'; gardez les jetons de session dans des cookiesHttpOnlyplutôt que danslocalStorage.