07 - XSS, CSRF, injection : les failles classiques
Ce que tu vas apprendre
- Les trois types de XSS et comment s'en protéger
- Comment fonctionne une attaque CSRF
- L'injection SQL et les requêtes parametrees
- Les headers de sécurité a configurer
Prerequisites
Avoir lu les articles précédents de la serie, en particulier sessions et cookies pour comprendre les protections cote cookie.
XSS : quand le navigateur exécuté le code de l'attaquant
XSS, c'est Cross-Site Scripting (oui, ca devrait etre CSS, mais c'etait deja pris). Le principe : un attaquant reussit a injecter du JavaScript dans une page web, et le navigateur de la victime l'exécuté comme si c'etait du code legitime.
Il y a trois variantes.
XSS stocke (stored). L'attaquant soumet du code malveillant qui est sauvegarde en base de donnees. Un commentaire de blog, un nom d'utilisateur, un champ de formulaire. Chaque utilisateur qui affiche ce contenu exécuté le script. C'est le plus dangereux.
XSS reflete (reflected). Le code malveillant est dans l'URL. L'attaquant envoie un lien piège (monsite.com/search?q=<script>voleMesCookies()</script>). Si le serveur renvoie le paramètre dans la page sans l'echapper, le script s'exécuté.
XSS DOM-based. Le code JavaScript de la page lit une source non fiable (URL, localStorage) et l'injecte dans le DOM sans l'echapper.
La protection, c'est toujours la meme chose : ne jamais insérer du contenu utilisateur dans le HTML sans l'echapper.
typescript// BAD : injection directe dans le DOM
const commentDiv = document.getElementById("comment");
commentDiv.innerHTML = userComment; // Si userComment contient <script>...</script>, c'est fini
// GOOD : utiliser textContent (echappe automatiquement)
commentDiv.textContent = userComment;
// GOOD cote serveur : echapper avant de renvoyer
function escapeHtml(str: string): string {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Le header Content-Security-Policy est ta deuxieme ligne de defense. Il dit au navigateur quels scripts il a le droit d'exécuter :
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
Avec cette politique, meme si un attaquant injecte un <script>, le navigateur refuse de l'exécuter parce qu'il ne vient pas de ton domaine. La doc MDN sur CSP explique toutes les directives en détail.
Et les cookies HttpOnly empechent le vol de session par XSS, comme on l'a vu dans l'article sur les cookies. Meme si un script s'exécuté, il ne peut pas lire le cookie de session.
CSRF : le navigateur trahi
CSRF, c'est Cross-Site Request Forgery. L'attaquant n'a pas besoin d'injecter du code dans ton site. Il exploite le fait que le navigateur envoie automatiquement les cookies a chaque requête.
Le scénario : tu es connecte a ta banque. Tu ouvres un autre onglet avec un site malveillant. Ce site contient un formulaire invisible qui fait un POST vers https://mabanque.com/transfer?to=attaquant&amount=10000. Ton navigateur envoie la requête avec tes cookies de session. La banque pense que c'est toi.
html<!-- Sur le site de l'attaquant -->
<form action="https://mabanque.com/transfer" method="POST" id="evil">
<input type="hidden" name="to" value="attaquant" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('evil').submit();</script>
Les protections :
SameSite=Strict sur les cookies. Le navigateur n'envoie pas le cookie si la requête vient d'un autre site. C'est la protection la plus simple et la plus efficace. Sur paltemps.fr, c'est ce que j'utilise.
Token CSRF. Le serveur généré un token aleatoire, l'inclut dans le formulaire (champ hidden), et vérifié qu'il est present dans la requête. Un site tiers ne peut pas connaître ce token.
typescript// Generer le token
const csrfToken = crypto.randomUUID();
// Stocker en session
session.csrfToken = csrfToken;
// Dans le formulaire HTML
// <input type="hidden" name="_csrf" value="{{csrfToken}}" />
// Verifier a la reception
app.post("/api/transfer", (ctx) => {
if (ctx.body._csrf !== ctx.session.csrfToken) {
return new Response("CSRF token mismatch", { status: 403 });
}
// ...
});
Verification du header Origin/Referer. Le serveur vérifié que la requête vient bien de son propre domaine.
Injection SQL : le classique des classiques
L'injection SQL est probablement la faille la plus connue, et elle existe toujours. Le principe : l'attaquant inséré du SQL dans un champ de saisie, et le serveur l'exécuté.
typescript// BAD : concatenation de l'input utilisateur dans la requete
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Si email = "' OR '1'='1' --", la requete devient :
// SELECT * FROM users WHERE email = '' OR '1'='1' --'
// Ca retourne TOUS les utilisateurs
// GOOD : requete parametree
const user = await db.query("SELECT * FROM users WHERE email = $1", [email]);
// Le driver echappe automatiquement. Impossible d'injecter du SQL.
La regle est absolue : ne concatene jamais d'input utilisateur dans une requête SQL. Utilise des requêtes parametrees. Tous les drivers modernes les supportent. Les ORM (Prisma, Drizzle, TypeORM) generent des requêtes parametrees par défaut.
La meme logique s'applique aux injections NoSQL (MongoDB), aux injections LDAP, et aux injections de commandes shell (exec, spawn). Tout input utilisateur est hostile jusqu'a preuve du contraire.
Les headers de sécurité
Sur paltemps.fr, le fichier security.ts ajoute ces headers a toutes les réponses :
typescriptheaders.set("X-Content-Type-Options", "nosniff");
headers.set("X-Frame-Options", "DENY");
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
// En production
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
X-Content-Type-Options: nosniff empeche le navigateur de "deviner" le type MIME d'un fichier. Sans ce header, un fichier .txt contenant du JavaScript pourrait etre exécuté.
X-Frame-Options: DENY interdit l'inclusion de ta page dans un <iframe>. Ca protégé contre le clickjacking (un site malveillant superpose ton site dans un iframe transparent et piège les clics).
Strict-Transport-Security (HSTS) dit au navigateur de ne jamais utiliser HTTP, seulement HTTPS. Meme si l'utilisateur tape http://, le navigateur redirige automatiquement vers https://.
Referrer-Policy contrôle combien d'information est envoyee dans le header Referer quand l'utilisateur quitte ton site.
Rate limiting et brute-force
Sur paltemps.fr, chaque tentative de login echouee est comptee par IP. Apres 5 échecs, l'IP est bloquee pendant 15 minutes :
typescriptconst MAX_LOGIN_FAILURES = 5;
const BLOCK_DURATION_MS = 15 * 60 * 1000; // 15 min
export function checkLoginBlocked(ip: string): { blocked: boolean; retryAfter: number } {
const entry = loginAttempts.get(ip);
if (!entry) return { blocked: false, retryAfter: 0 };
if (Date.now() < entry.blockedUntil) {
return { blocked: true, retryAfter: Math.ceil((entry.blockedUntil - Date.now()) / 1000) };
}
return { blocked: false, retryAfter: 0 };
}
C'est la base de la protection contre le brute-force. En production plus serieuse, tu ajouterais du rate limiting global (par IP, par route) et eventuellement un CAPTCHA apres N échecs. Consulte la serie secrets et variables d'environnement pour la gestion sécurisée des identifiants de ces services.
L'OWASP Top 10
L'OWASP Top 10 est la référencé des risques de sécurité web. La version 2021 liste :
- Broken Access Control (ce qu'on a vu dans l'article RBAC)
- Cryptographic Failures (serie clés et chiffrement)
- Injection (SQL, NoSQL, OS command)
- Insecure Design
- Security Misconfiguration
Je te recommande de lire le document complet. C'est gratuit, bien écrit, avec des exemples concrets.
Navigation : Precedent : 06 - RBAC | Suivant : 08 - Glossaire
Sources
- OWASP Top 10 (2021) par OWASP
- MDN - Content Security Policy par Mozilla
- MDN - SameSite cookies par Mozilla
- PortSwigger - Cross-Site Scripting par PortSwigger
Retrouve d'autres articles techniques sur paltemps.fr.