Contactez-nous

Sécurité : prévention des attaques courantes (XSS, CSRF, SQL injection)

Protégez vos applications Node.js contre les attaques web courantes : Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF) et injections SQL. Apprenez les techniques de prévention.

La sécurité : une préoccupation majeure pour les applications Node.js

Développer une application Node.js fonctionnelle est une chose, mais assurer sa sécurité en est une autre, bien plus critique. Les applications web sont constamment exposées à diverses menaces, et une faille de sécurité peut avoir des conséquences dévastatrices : vol de données utilisateurs, perte financière, atteinte à la réputation, indisponibilité du service, voire compromission complète du serveur.

Il est donc impératif pour tout développeur Node.js d'avoir une compréhension solide des vulnérabilités web courantes et des moyens de s'en prémunir. Ignorer la sécurité n'est pas une option. Ce chapitre se concentre sur trois des attaques les plus fréquentes et dangereuses : le Cross-Site Scripting (XSS), le Cross-Site Request Forgery (CSRF) et les Injections SQL (SQLi), en expliquant leur fonctionnement et les stratégies de prévention spécifiques à l'environnement Node.js.

Adopter une mentalité axée sur la sécurité dès le début du développement ('security by design') et appliquer rigoureusement les bonnes pratiques permet de construire des applications plus résilientes face aux attaques.

Cross-Site Scripting (XSS) : L'injection de scripts malveillants

Qu'est-ce que c'est ? Le XSS est une vulnérabilité qui permet à un attaquant d'injecter des scripts malveillants (généralement JavaScript) dans des pages web consultées par d'autres utilisateurs. Le navigateur de la victime exécute alors ce script comme s'il provenait légitimement du site web, lui donnant accès aux cookies de session, au contenu de la page, etc.

Types courants :

  • XSS stocké (Stored XSS) : Le script malveillant est stocké sur le serveur (ex: dans un commentaire de blog, un profil utilisateur) et servi à tous les visiteurs de la page concernée.
  • XSS réfléchi (Reflected XSS) : Le script est injecté via une requête (ex: un paramètre d'URL) et renvoyé immédiatement par le serveur dans la réponse. L'attaquant doit généralement inciter la victime à cliquer sur un lien forgé.
  • XSS basé sur le DOM (DOM-based XSS) : La vulnérabilité se situe dans le code JavaScript côté client qui manipule le DOM de manière non sécurisée à partir de données contrôlables par l'utilisateur (ex: `document.location.hash`).

Impact : Vol de cookies de session (détournement de session), phishing, modification du contenu de la page (défaçage), redirection vers des sites malveillants, enregistrement des frappes clavier (keylogging).

Prévention en Node.js :

  • Encodage/Echappement des sorties (Output Encoding/Escaping) : C'est la défense la plus importante. Avant d'afficher des données provenant d'une source non fiable (utilisateur, base de données, API externe) dans une page HTML, encodez systématiquement les caractères spéciaux HTML (`, &, <, >, "`). La plupart des moteurs de template (EJS, Pug, Handlebars) le font automatiquement par défaut pour les variables insérées. Si vous construisez du HTML manuellement ou si vous insérez des données dans des contextes dangereux (attributs `href`, `src`, styles en ligne), utilisez des bibliothèques d'encodage dédiées comme `he` ou les fonctions d'échappement spécifiques de votre moteur de template.
// Exemple avec EJS (échappement par défaut)
// Supposons que userInput = ""

Commentaire: <%= userInput %>

// Résultat HTML rendu (sécurisé) : //

Commentaire: <script>alert('XSS!');</script>

// Utilisation de la bibliothèque 'he' pour encodage manuel const he = require('he'); const unsafeData = ""; const safeData = he.encode(unsafeData); // safeData = "<script>alert('XSS!');</script>" console.log(`
Affichage
`);
  • Content Security Policy (CSP) : Utilisez l'en-tête HTTP `Content-Security-Policy` pour définir des règles strictes sur les sources de contenu autorisées (scripts, styles, images, etc.). Cela peut empêcher l'exécution de scripts injectés, même si l'encodage a échoué. Le middleware `helmet` facilite la configuration du CSP.
const helmet = require('helmet');
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"], // N'autorise que le contenu du même domaine par défaut
      scriptSrc: ["'self'", 'trusted-cdn.com'], // Autorise scripts locaux et d'un CDN fiable
      // ... autres directives (styleSrc, imgSrc, etc.)
    },
  })
);
  • Validation des entrées (Input Validation) : Validez et nettoyez les données reçues des utilisateurs (longueur, format, type). Bien que cela puisse bloquer certaines tentatives simples, ce n'est pas une défense suffisante contre XSS. L'encodage des sorties reste primordial.
  • Cookies `HttpOnly` : Configurez vos cookies de session avec l'attribut `HttpOnly`. Cela empêche le JavaScript côté client d'y accéder, rendant le vol de session via XSS beaucoup plus difficile.
// Avec express-session
app.use(session({
  secret: 'votre_secret',
  resave: false,
  saveUninitialized: true,
  cookie: { 
    secure: true, // Seulement sur HTTPS
    httpOnly: true, // Empêche l'accès JS
    sameSite: 'lax' // Protection CSRF
  }
}));

Injection SQL (SQLi) : Manipuler les requêtes de base de données

Qu'est-ce que c'est ? L'injection SQL se produit lorsqu'un attaquant peut insérer ou 'injecter' des commandes SQL malveillantes dans les requêtes envoyées à la base de données, généralement via des entrées utilisateur non validées ou non échappées.

Impact : Contournement de l'authentification, lecture de données sensibles (toute la base), modification ou suppression de données, exécution de commandes système sur le serveur de base de données (selon les privilèges).

Prévention en Node.js :

  • Requêtes préparées (Prepared Statements) / Requêtes paramétrées (Parameterized Queries) : C'est LA défense principale et la plus efficace. Au lieu de concaténer directement les entrées utilisateur dans la chaîne SQL, utilisez des placeholders (marqueurs `?` ou `$1`, `$2`...) dans votre requête SQL. Fournissez ensuite les valeurs utilisateur séparément à la méthode d'exécution de la requête. Le pilote de base de données (ex: `mysql2`, `pg`) ou l'ORM s'assure que ces valeurs sont traitées comme des données et non comme du code SQL exécutable, neutralisant ainsi l'injection.
// MAUVAIS : Vulnérable à l'injection SQL
const userId = req.query.id; // Ex: "1' OR '1'='1" 
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// db.query(query, ...); // Exécutera SELECT * FROM users WHERE id = '1' OR '1'='1'

// BON : Utilisation de requêtes paramétrées (avec node-postgres/pg)
const userId = req.query.id; 
const queryText = 'SELECT * FROM users WHERE id = $1';
const values = [userId];
db.query(queryText, values)
  .then(res => { /* ... */ })
  .catch(err => { /* ... */ });

// BON : Utilisation de requêtes préparées (avec mysql2)
const userId = req.query.id;
const sql = 'SELECT * FROM users WHERE id = ?';
connection.query(sql, [userId], (error, results) => {
  if (error) throw error;
  // ... traiter results ...
});
  • Utilisation d'ORMs / ODMs : Des bibliothèques comme Sequelize (SQL) ou Mongoose (MongoDB - protège contre les injections NoSQL) construisent généralement les requêtes de manière sécurisée en utilisant des mécanismes similaires aux requêtes préparées, à condition de les utiliser correctement (ne pas passer de chaînes SQL brutes construites manuellement avec des entrées utilisateur).
  • Validation et Sanitarisation des entrées (Défense secondaire) : Validez toujours le type et le format des données attendues (ex: un ID doit être un nombre). La sanitarisation (tenter de nettoyer les entrées) est complexe et moins fiable que les requêtes préparées. C'est une défense en profondeur, pas la principale.
  • Principe du moindre privilège : Configurez l'utilisateur de base de données utilisé par votre application Node.js avec les permissions minimales nécessaires. Il ne devrait pas pouvoir supprimer des tables s'il n'a besoin que de lire et écrire des données.

Cross-Site Request Forgery (CSRF) : Forcer des actions indésirables

Qu'est-ce que c'est ? Le CSRF (parfois prononcé 'sea-surf') est une attaque qui force le navigateur d'un utilisateur authentifié à envoyer une requête HTTP non désirée (généralement une requête qui modifie l'état, comme POST, PUT, DELETE) à une application web sur laquelle l'utilisateur est déjà connecté. L'attaquant prépare une page web ou un email malveillant qui déclenche cette requête à l'insu de la victime.

Comment ça marche ? Les navigateurs incluent automatiquement les cookies (y compris les cookies de session) pour un domaine donné lors de toute requête vers ce domaine, même si la requête est initiée depuis un autre site (cross-site). L'attaquant exploite cette confiance implicite.

Impact : Actions non autorisées effectuées au nom de la victime : modification de l'email ou du mot de passe, publication de contenu, suppression de données, transfert de fonds, etc.

Prévention en Node.js :

  • Pattern du Jeton Synchronizer (Synchronizer Token Pattern / Anti-CSRF Tokens) : C'est la défense la plus courante et la plus robuste. Le principe est le suivant :
    1. Pour chaque session utilisateur, le serveur génère un token secret unique et imprévisible (jeton CSRF).
    2. Ce token est stocké côté serveur (associé à la session) et également intégré dans toutes les pages HTML contenant des formulaires qui effectuent des actions sensibles (souvent via un champ caché ``). Pour les requêtes AJAX, le token est souvent envoyé via un en-tête HTTP personnalisé (ex: `X-CSRF-Token`).
    3. Lorsqu'une requête sensible (POST, PUT, DELETE) arrive, le serveur vérifie que le token soumis dans la requête correspond bien au token stocké dans la session de l'utilisateur.
    Si les tokens correspondent, la requête est légitime. Si le token est manquant ou incorrect, la requête est rejetée. Un attaquant ne peut pas connaître le token secret de la victime, donc il ne peut pas forger une requête valide. Des middlewares comme `csurf` (bien que sa maintenance puisse être moins active, le concept reste valide) implémentent ce pattern pour Express, mais on peut aussi l'implémenter manuellement.
// Concept avec csurf (vérifier la documentation actuelle et alternatives si besoin)
const csrf = require('csurf');
const session = require('express-session');
// ... configuration session ...

const csrfProtection = csrf({ cookie: true }); // Stocke le secret dans un cookie
app.use(csrfProtection);

app.get('/mon-formulaire', (req, res) => {
  // Passer le token CSRF à la vue
  res.render('mon-formulaire-template', { csrfToken: req.csrfToken() });
});

app.post('/process', (req, res) => {
  // csurf middleware a déjà vérifié le token soumis (_csrf dans le corps du formulaire)
  res.send('Données traitées avec succès (CSRF OK)');
});

// Dans mon-formulaire-template.ejs
// 
// // // //
  • Attribut `SameSite` pour les Cookies : Cet attribut contrôle si les cookies doivent être envoyés avec les requêtes cross-site.
    • `SameSite=Strict` : Le cookie n'est envoyé que pour les requêtes provenant du même site. Très sûr, mais peut casser des liens légitimes entrants vers des actions POST.
    • `SameSite=Lax` : Le cookie est envoyé pour la navigation de premier niveau (clic sur un lien) même cross-site, mais pas pour les requêtes cross-site initiées par des scripts, iframes, ou formulaires POST. C'est un bon compromis et le défaut dans de nombreux navigateurs modernes. Offre une protection significative.
    • `SameSite=None` (avec `Secure`) : Le cookie est envoyé pour toutes les requêtes (nécessaire pour certains cas d'usage inter-domaines, mais désactive la protection CSRF via SameSite).
    Configurer `SameSite=Lax` ou `Strict` pour vos cookies de session est une excellente défense en profondeur contre CSRF.
  • Vérification des en-têtes `Origin` ou `Referer` : Vérifier que ces en-têtes (s'ils sont présents) correspondent au domaine de votre application peut offrir une protection supplémentaire, mais ils ne sont pas toujours présents ou fiables et ne doivent pas être la seule défense.
  • Protection des requêtes AJAX : Pour les API utilisées par du JavaScript frontend, la combinaison de tokens CSRF (souvent via en-tête personnalisé) et des contrôles CORS (Cross-Origin Resource Sharing) bien configurés est essentielle.

Il est crucial de protéger toutes les requêtes qui modifient l'état ou effectuent des actions sensibles. Les requêtes GET sont généralement considérées comme sûres (elles ne devraient pas modifier de données), mais il faut s'assurer qu'elles n'ont pas d'effets de bord indésirables.

Conclusion : Une approche multicouche de la sécurité

La prévention des attaques comme XSS, SQLi et CSRF en Node.js repose sur une combinaison de bonnes pratiques et l'utilisation correcte des outils et mécanismes de défense.

  • Contre XSS : Encodage systématique des sorties, CSP, cookies HttpOnly.
  • Contre SQLi : Requêtes préparées/paramétrées (priorité absolue), ORM/ODM utilisés correctement, moindre privilège.
  • Contre CSRF : Tokens Anti-CSRF (Synchronizer Token Pattern), attribut `SameSite` pour les cookies.

Au-delà de ces trois menaces, n'oubliez pas les autres aspects de la sécurité : maintenir les dépendances à jour (`npm audit`), utiliser HTTPS, mettre en place une authentification et une autorisation robustes, utiliser des en-têtes de sécurité via `helmet`, limiter le taux de requêtes (rate limiting), et gérer les erreurs proprement sans fuite d'informations sensibles. La sécurité est un processus continu et une responsabilité partagée à tous les niveaux du développement.