Les en-têtes de sécurité HTTP constituent la première ligne de défense d’une application web. Pourtant, selon l’OWASP Top 10, la mauvaise configuration de sécurité (A05) touche la majorité des applications testées, et l’absence d’en-têtes HTTP adaptés reste l’une des failles les plus fréquentes et les plus faciles à corriger. En Node.js, la bibliothèque Helmet.js 8.1.0 (plus de 2 millions de téléchargements hebdomadaires sur npm) permet d’ajouter 12 en-têtes de sécurité en une seule ligne de code.
Ce tutoriel couvre chaque en-tête en détail, sa configuration avec Helmet, les pièges courants et la vérification de votre score de sécurité. À la fin, vous disposerez d’un projet Express complet, configuré selon les standards 2026, prêt pour la production.
Pourquoi les en-têtes de sécurité sont essentiels en 2026
Un en-tête HTTP de sécurité est une directive envoyée par le serveur au navigateur du client. Il indique au navigateur comment traiter le contenu de la page : quelles sources de scripts autoriser, si la connexion doit être chiffrée, si la page peut être embarquée dans une iframe, etc. Sans ces en-têtes, le navigateur applique des comportements par défaut souvent permissifs qui ouvrent la porte aux attaques les plus répandues.
Les attaques directement liées à des en-têtes manquants ou mal configurés incluent : le cross-site scripting (XSS), le clickjacking, le MIME sniffing, les attaques de type downgrade HTTPS, et la fuite d’informations via le Referer. La directive A05 de l’OWASP Top 10 2021 classe explicitement la mauvaise configuration de sécurité parmi les 5 risques les plus critiques pour les applications web. La référence complète se trouve dans la HTTP Headers Cheat Sheet de l’OWASP.
En Node.js avec Express, les en-têtes de sécurité ne sont pas activés par défaut. Un serveur Express vierge renvoie des en-têtes minimaux, dont le révélateur X-Powered-By: Express qui annonce la technologie utilisée aux attaquants. La solution standardisée dans l’écosystème Node.js est Helmet.js, un middleware qui configure automatiquement 12 en-têtes de sécurité et supprime X-Powered-By. Avec plus de 2 millions de téléchargements hebdomadaires sur npm, Helmet est utilisé dans la quasi-totalité des applications Express en production.
La dernière version stable, Helmet 8.1.0 (sortie le 17 mars 2025), est celle que nous utilisons dans ce tutoriel. Elle apporte une configuration plus granulaire des en-têtes Cross-Origin et un meilleur support des politiques de permissions modernes.
Prérequis et versions
Avant de commencer, vérifiez que votre environnement correspond aux versions suivantes :
| Technologie | Version minimale | Version recommandée 2026 |
|---|---|---|
| Node.js | 18.x LTS | 22.x LTS |
| npm | 9.x | 10.x |
| Express | 4.18.x | 5.x |
| Helmet.js | 7.x | 8.1.0 |
| Système d’exploitation | Linux / macOS / Windows | Ubuntu 24.04 LTS |
Connaissances requises : bases de JavaScript et de Node.js, utilisation basique de la ligne de commande, notions fondamentales de HTTP. Si vous débutez avec l’authentification Node.js, consultez d’abord notre guide Authentification JWT en Node.js : 12 étapes.
Étape 1 : Initialiser le projet Node.js
Créez un dossier de travail et initialisez un nouveau projet Node.js avec npm :
mkdir node-security-headers
cd node-security-headers
npm init -y
npm install express helmet
Cette commande installe Express et Helmet dans leurs dernières versions stables. Vérifiez les versions installées :
npm list express helmet
Sortie attendue :
[email protected]
├── [email protected]
└── [email protected]
Créez ensuite le fichier principal app.js avec une structure de base :
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Serveur Node.js sans sécurité' });
});
app.listen(3000, () => {
console.log('Serveur démarré sur http://localhost:3000');
});
Lancez le serveur et examinez les en-têtes renvoyés sans Helmet :
node app.js &
curl -I http://localhost:3000/
Sortie typique d’un serveur Express sans Helmet :
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 42
Date: Wed, 18 Jun 2026 10:00:00 GMT
Connection: keep-alive
On constate immédiatement l’absence totale d’en-têtes de sécurité et la présence de X-Powered-By: Express qui expose la technologie utilisée. Un attaquant qui récupère cet en-tête peut cibler directement les vulnérabilités connues de la version d’Express utilisée.
Étape 2 : Intégrer Helmet avec la configuration par défaut
L’intégration de base de Helmet.js requiert deux lignes de code. Modifiez app.js :
const express = require('express');
const helmet = require('helmet');
const app = express();
// Helmet doit être placé avant les routes
app.use(helmet());
app.get('/', (req, res) => {
res.json({ message: 'Serveur Node.js sécurisé avec Helmet' });
});
app.listen(3000, () => {
console.log('Serveur démarré sur http://localhost:3000');
});
Redémarrez et vérifiez les en-têtes avec Helmet actif :
node app.js &
curl -I http://localhost:3000/
Sortie avec Helmet en configuration par défaut :
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0
Content-Type: application/json; charset=utf-8
En une ligne, app.use(helmet()) a ajouté 12 en-têtes de sécurité et supprimé X-Powered-By. Le tableau suivant résume le rôle de chaque en-tête :
| En-tête HTTP | Valeur par défaut Helmet 8.x | Protection contre |
|---|---|---|
| Content-Security-Policy | default-src ‘self’ + directives multiples | XSS, injection de code |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | Downgrade HTTPS, SSL stripping |
| X-Frame-Options | SAMEORIGIN | Clickjacking |
| X-Content-Type-Options | nosniff | MIME sniffing |
| Referrer-Policy | no-referrer | Fuite d’URL sensibles |
| Cross-Origin-Embedder-Policy | require-corp | Cross-origin data leaks |
| Cross-Origin-Opener-Policy | same-origin | Cross-origin window attacks |
| Cross-Origin-Resource-Policy | same-origin | Spectre, cross-site reads |
| X-DNS-Prefetch-Control | off | Fuite DNS |
| X-Download-Options | noopen | Exécution automatique IE |
| X-Permitted-Cross-Domain-Policies | none | Flash, PDF cross-domain |
| X-XSS-Protection | 0 (désactivé délibérément) | N/A (obsolète, dangereux en mode actif) |
Étape 3 : Configurer Content-Security-Policy (CSP)
La Content-Security-Policy (CSP) est l’en-tête le plus puissant et le plus complexe à configurer. Elle définit les sources autorisées pour chaque type de ressource : scripts, styles, images, polices, iframes, etc. Un CSP mal configuré peut soit bloquer les ressources légitimes de votre application, soit laisser des failles XSS ouvertes. La spécification MDN sur la Content-Security-Policy liste l’ensemble des directives disponibles.
En pratique, les directives les plus importantes sont :
| Directive CSP | Rôle | Valeur sécurisée recommandée |
|---|---|---|
| default-src | Source par défaut pour tout type de ressource | ‘self’ |
| script-src | Sources des scripts JavaScript | ‘self’ + nonce pour scripts inline |
| style-src | Sources des feuilles de style CSS | ‘self’ https: |
| img-src | Sources des images | ‘self’ data: https: |
| font-src | Sources des polices de caractères | ‘self’ https: data: |
| connect-src | Sources pour fetch(), XHR, WebSocket | ‘self’ |
| frame-ancestors | Qui peut embarquer la page dans une iframe | ‘none’ ou ‘self’ |
| object-src | Sources pour Flash, plugins | ‘none’ |
| base-uri | Restreint la balise <base> | ‘self’ |
| upgrade-insecure-requests | Force le chargement HTTPS des ressources HTTP | Actif en production |
Voici une configuration CSP réaliste pour une application Express avec API REST et ressources statiques :
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'", 'https:', 'data:'],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
reportUri: '/csp-report',
},
},
})
);
// Endpoint pour recevoir les rapports de violations CSP
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('Violation CSP détectée:', JSON.stringify(req.body, null, 2));
res.status(204).end();
});
app.get('/', (req, res) => {
res.json({ message: 'API sécurisée avec CSP' });
});
app.listen(3000);
Pour les applications qui utilisent des CDN (Google Fonts, Bootstrap, etc.), ajoutez explicitement ces domaines dans les directives concernées. Si votre front-end utilise des scripts inline ou des gestionnaires d’événements inline, l’approche recommandée en 2026 est d’utiliser des nonces CSP plutôt que 'unsafe-inline'.
CSP en mode rapport uniquement (recommandé pour le déploiement initial)
Pendant le déploiement initial, activez d’abord le mode rapport (Content-Security-Policy-Report-Only) pour détecter les violations sans bloquer les ressources légitimes :
app.use(
helmet({
contentSecurityPolicy: {
reportOnly: true, // Observe les violations sans bloquer
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
reportUri: '/csp-report',
},
},
})
);
Consultez les logs des violations pendant 24 à 48 heures, ajustez les directives pour les sources légitimes, puis basculez en mode enforcement en supprimant reportOnly: true. Cette approche évite de bloquer des ressources légitimes en production.
Étape 4 : HTTP Strict Transport Security (HSTS)
L’en-tête Strict-Transport-Security (HSTS) force le navigateur à utiliser exclusivement HTTPS pour toutes les communications avec votre domaine pendant une durée définie. Une fois que le navigateur a reçu cet en-tête, il refuse toute connexion HTTP et convertit automatiquement toutes les requêtes en HTTPS, même si l’utilisateur tape http:// dans la barre d’adresse.
HSTS protège contre les attaques de type SSL stripping, où un attaquant placé en position de man-in-the-middle intercepte la connexion initiale HTTP avant la redirection vers HTTPS. La configuration par défaut de Helmet définit un HSTS de 365 jours (31 536 000 secondes) avec includeSubDomains. Pour la production, ajoutez preload pour soumettre votre domaine à la liste de préchargement HSTS intégrée aux navigateurs :
app.use(
helmet({
hsts: {
maxAge: 31536000, // 365 jours en secondes
includeSubDomains: true, // Applique HSTS à tous les sous-domaines
preload: true, // Soumet le domaine à la liste HSTS preload
},
})
);
Points critiques sur la configuration HSTS :
- N’activez jamais HSTS avant d’avoir un certificat SSL valide. Si votre HTTPS tombe en panne avec HSTS actif, les utilisateurs ne pourront plus accéder au site pendant toute la durée de
maxAge. - Commencez avec un
maxAgecourt (86 400 secondes = 1 jour) pendant les tests, puis augmentez progressivement jusqu’à 31 536 000. includeSubDomains: activez cette option uniquement si tous vos sous-domaines supportent HTTPS.- L’option
preloadest irréversible à court terme : une fois soumis, le retrait de votre domaine de la liste peut prendre des mois. Utilisez-la uniquement quand vous êtes certain de maintenir HTTPS indéfiniment.
Pour approfondir la configuration TLS, consultez notre comparatif TLS 1.3 vs TLS 1.2 : 40% plus rapide, 5 CVE et notre tutoriel OpenSSL : clés et certificats en 12 étapes.
Étape 5 : X-Frame-Options et protection contre le clickjacking
Le clickjacking est une attaque où un attaquant embarque votre page dans une iframe invisible superposée à une interface trompeuse. L’utilisateur pense cliquer sur un bouton inoffensif, mais clique en réalité sur un élément de votre application (confirmation de virement bancaire, validation de commande, modification de paramètres de sécurité, etc.).
Helmet configure par défaut X-Frame-Options: SAMEORIGIN, ce qui autorise l’embarquement dans une iframe uniquement depuis la même origine. Les trois valeurs possibles sont :
| Valeur X-Frame-Options | Effet | Cas d’usage recommandé |
|---|---|---|
| DENY | Interdit toute iframe, quelle que soit l’origine | Pages de connexion, paiement, administration |
| SAMEORIGIN | Autorise uniquement la même origine | Applications web standard |
| ALLOW-FROM uri | Autorise une URI spécifique (obsolète) | Ne pas utiliser en 2026 |
Pour une protection maximale sur les pages sensibles, utilisez DENY :
// Configuration globale DENY
app.use(
helmet({
frameguard: {
action: 'deny',
},
})
);
// Ou par route spécifique pour les pages les plus sensibles
const sensitiveHelmet = helmet({ frameguard: { action: 'deny' } });
app.use('/login', sensitiveHelmet);
app.use('/payment', sensitiveHelmet);
app.use('/admin', sensitiveHelmet);
En 2026, la directive CSP frame-ancestors est préférée à X-Frame-Options pour les navigateurs modernes, car elle offre plus de flexibilité (plusieurs origines autorisées simultanément). Helmet active les deux par défaut pour garantir la compatibilité avec les navigateurs plus anciens. En cas de conflit, frame-ancestors a la priorité dans les navigateurs qui le supportent.
Étape 6 : X-Content-Type-Options et MIME sniffing
L’attaque par MIME sniffing exploite le comportement de certains navigateurs anciens qui ignorent le type de contenu déclaré (Content-Type) et tentent de deviner le type réel du fichier. Un attaquant peut télécharger un fichier apparemment inoffensif (image, document texte) qui contient en réalité du code JavaScript exécutable.
L’en-tête X-Content-Type-Options: nosniff interdit au navigateur de faire du MIME sniffing et l’oblige à respecter strictement le Content-Type déclaré par le serveur. Il s’agit de l’en-tête le plus simple à déployer, avec un impact zéro sur les fonctionnalités légitimes si vos routes Express déclarent correctement leurs types MIME :
// Activé par défaut avec helmet()
// Configuration explicite si nécessaire :
app.use(
helmet({
noSniff: true, // X-Content-Type-Options: nosniff
})
);
// Bonne pratique complémentaire : déclarer explicitement le Content-Type
app.get('/api/data', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.json({ data: 'valeur' });
});
app.get('/document.pdf', (req, res) => {
res.setHeader('Content-Type', 'application/pdf');
res.sendFile('./documents/rapport.pdf');
});
app.get('/image.png', (req, res) => {
res.setHeader('Content-Type', 'image/png');
res.sendFile('./public/logo.png');
});
Étape 7 : Referrer-Policy et protection de la vie privée
L’en-tête Referrer-Policy contrôle quelle information de l’URL courante est transmise dans l’en-tête Referer lorsque l’utilisateur clique sur un lien ou que votre page effectue des requêtes externes. Sans cet en-tête, les URLs complètes (incluant les paramètres de requête sensibles) peuvent être transmises à des serveurs tiers.
Scénario concret de fuite : votre application utilise des URLs comme https://monapp.com/reset-password?token=abc123secret. Si la page de réinitialisation contient une image hébergée sur un serveur tiers (analytics, CDN), le token peut apparaître dans les logs du serveur tiers via l’en-tête Referer.
Helmet configure par défaut Referrer-Policy: no-referrer. Les valeurs courantes :
| Valeur Referrer-Policy | Information transmise | Recommandé pour |
|---|---|---|
| no-referrer | Aucune information | API, pages avec tokens dans URL |
| strict-origin | Origine seule (HTTPS uniquement) | Bon compromis général |
| strict-origin-when-cross-origin | URL complète en same-origin, origine seule en cross-origin | Apps avec analytics first-party |
| same-origin | URL complète uniquement en same-origin | Applications internes fermées |
| no-referrer-when-downgrade | URL complète sauf HTTPS vers HTTP | A éviter |
| unsafe-url | URL complète dans tous les cas | Ne jamais utiliser |
app.use(
helmet({
referrerPolicy: {
// Pour une API REST pure : no-referrer (défaut Helmet)
// Pour une app web avec analytics : strict-origin-when-cross-origin
policy: 'strict-origin-when-cross-origin',
},
})
);
Étape 8 : Permissions-Policy
L’en-tête Permissions-Policy (anciennement Feature-Policy) contrôle quelles fonctionnalités du navigateur votre application est autorisée à utiliser, et si des iframes tierces peuvent en bénéficier. Il permet de désactiver des APIs potentiellement dangereuses ou inutiles : accès caméra, microphone, géolocalisation, capteurs de l’appareil, API Payment, etc.
Helmet 8.x n’inclut pas de Permissions-Policy par défaut. Configurez-le via un middleware personnalisé selon les besoins réels de votre application :
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(helmet());
// Permissions-Policy : désactiver les APIs non utilisées
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
[
'camera=()', // Désactive l'accès caméra
'microphone=()', // Désactive l'accès microphone
'geolocation=()', // Désactive la géolocalisation
'payment=()', // Désactive l'API Payment (si non marchand)
'usb=()', // Désactive l'accès USB
'interest-cohort=()', // Désactive le tracking comportemental
'accelerometer=()', // Désactive les capteurs de mouvement
'gyroscope=()',
'magnetometer=()',
].join(', ')
);
next();
});
Pour une application e-commerce qui utilise réellement l’API Payment, adaptez la politique en autorisant uniquement depuis votre propre origine :
// E-commerce : autoriser les paiements depuis l'origine de l'app uniquement
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
[
'camera=()',
'microphone=()',
'geolocation=()',
'payment=(self)', // Autorisé uniquement depuis votre propre origine
'usb=()',
].join(', ')
);
next();
});
Étape 9 : En-têtes Cross-Origin (COEP, COOP, CORP)
Depuis les découvertes des vulnérabilités Spectre et Meltdown, les navigateurs ont introduit trois en-têtes d’isolation cross-origin qui constituent une couche de protection supplémentaire contre l’extraction de données entre origines différentes. Helmet 8.x les active tous par défaut :
- Cross-Origin-Embedder-Policy (COEP) : Empêche un document d’embarquer des ressources cross-origin sans autorisation explicite. Valeur par défaut :
require-corp. - Cross-Origin-Opener-Policy (COOP) : Isole le contexte de navigation, empêchant les pages cross-origin d’accéder à l’objet
windowde votre page. Valeur par défaut :same-origin. - Cross-Origin-Resource-Policy (CORP) : Empêche d’autres origines de lire vos ressources via des balises
<img>,<script>, etc. Valeur par défaut :same-origin.
app.use(
helmet({
// Cross-Origin-Embedder-Policy
crossOriginEmbedderPolicy: {
policy: 'require-corp', // Stricte, peut casser les ressources CDN
// policy: 'unsafe-none', // Pour désactiver si vous utilisez des CDN
},
// Cross-Origin-Opener-Policy
crossOriginOpenerPolicy: {
policy: 'same-origin',
// policy: 'same-origin-allow-popups', // Si vous ouvrez des popups auth
},
// Cross-Origin-Resource-Policy
crossOriginResourcePolicy: {
policy: 'same-origin',
// policy: 'same-site', // Pour partager avec sous-domaines
// policy: 'cross-origin', // Pour ressources publiques (CDN, images publiques)
},
})
);
Si votre application embarque des ressources tierces (images CDN, iframes YouTube, widgets tiers), crossOriginEmbedderPolicy: 'require-corp' peut bloquer ces ressources. Dans ce cas, désactivez-le avec 'unsafe-none' en attendant que vos fournisseurs CDN supportent correctement CORS et CORP.
Étape 10 : Supprimer X-Powered-By et masquer les informations serveur
L’en-tête X-Powered-By: Express révèle aux attaquants la technologie utilisée, facilitant le ciblage avec des exploits spécifiques à Express ou Node.js. Helmet le supprime automatiquement via app.disable('x-powered-by'). Mais d’autres en-têtes peuvent également révéler des informations sur votre infrastructure :
// Helmet supprime X-Powered-By automatiquement.
// Pour supprimer d'autres en-têtes révélateurs :
app.use((req, res, next) => {
// Supprimer les en-têtes qui révèlent l'infrastructure
res.removeHeader('Server');
res.removeHeader('X-AspNet-Version');
res.removeHeader('X-AspNetMvc-Version');
next();
});
// Vérification post-configuration :
// Aucun de ces en-têtes ne doit apparaître en production
const forbiddenHeaders = ['x-powered-by', 'server', 'x-aspnet-version'];
app.use((req, res, next) => {
const originalEnd = res.end;
res.end = function(...args) {
forbiddenHeaders.forEach(h => {
if (res.getHeader(h)) {
console.warn(`[SECURITE] En-tête révélateur détecté: ${h}`);
}
});
originalEnd.apply(this, args);
};
next();
});
Pour les applications derrière un proxy inverse nginx, configurez également nginx pour masquer sa version :
# nginx.conf
server_tokens off; # Supprime la version dans l'en-tête Server
# Avec le module ngx_headers_more (si disponible) :
# more_clear_headers Server;
Étape 11 : Configuration complète pour la production
En production, la configuration Helmet doit être plus stricte qu’en développement. Voici une configuration complète et annotée, séparant les paramètres de développement et de production :
const express = require('express');
const helmet = require('helmet');
const app = express();
const isProduction = process.env.NODE_ENV === 'production';
// Configuration Helmet unifiée dev/prod
app.use(
helmet({
// Content-Security-Policy : plus strict en prod, mode rapport en dev
contentSecurityPolicy: isProduction
? {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
scriptSrcAttr: ["'none'"],
styleSrc: ["'self'", 'https:'],
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'", 'https:'],
connectSrc: ["'self'"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: [],
reportUri: '/csp-violations',
},
}
: {
reportOnly: true, // Mode observation uniquement en développement
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Plus permissif en dev
styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:', 'http:'],
reportUri: '/csp-violations',
},
},
// HSTS : activé uniquement en production (HTTPS requis)
hsts: isProduction
? {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
}
: false,
// Anti-clickjacking : DENY pour pages sensibles, SAMEORIGIN par défaut
frameguard: { action: isProduction ? 'deny' : 'sameorigin' },
// Toujours actifs en dev et prod
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
dnsPrefetchControl: { allow: false },
originAgentCluster: true,
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
xssFilter: false, // Désactivé intentionnellement (obsolète et dangereux)
// Cross-Origin : actifs en prod, désactivables en dev si besoin
crossOriginEmbedderPolicy: isProduction ? { policy: 'require-corp' } : false,
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: isProduction ? 'same-origin' : 'cross-origin' },
})
);
// Permissions-Policy
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=()'
);
next();
});
// Endpoint de rapport CSP
app.post(
'/csp-violations',
express.json({ type: 'application/csp-report' }),
(req, res) => {
if (req.body?.['csp-report']) {
console.warn('[CSP Violation]', JSON.stringify(req.body['csp-report']));
}
res.status(204).end();
}
);
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', env: process.env.NODE_ENV });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Serveur ${isProduction ? 'PRODUCTION' : 'DEV'} démarré sur le port ${PORT}`);
});
Étape 12 : Tester et valider vos en-têtes de sécurité
Mozilla Observatory est l’outil de référence pour évaluer la qualité de vos en-têtes de sécurité. Il attribue un score de A+ à F en analysant la présence et la configuration de chaque en-tête. Une configuration Helmet correcte vise au minimum un score B, idéalement A ou A+.
Pour tester votre application en local, utilisez d’abord curl :
# Vérifier la présence de tous les en-têtes de sécurité
curl -I --silent https://votre-domaine.com | grep -iE \
"content-security|strict-transport|x-frame|x-content-type|referrer|permissions|cross-origin"
# Sortie attendue en production :
# content-security-policy: default-src 'self';...
# strict-transport-security: max-age=31536000; includeSubDomains; preload
# x-frame-options: DENY
# x-content-type-options: nosniff
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: camera=(), microphone=(), geolocation=()
# cross-origin-embedder-policy: require-corp
# cross-origin-opener-policy: same-origin
# cross-origin-resource-policy: same-origin
Pour des tests automatisés dans votre pipeline CI/CD, utilisez supertest avec Jest :
npm install --save-dev supertest jest
// tests/security-headers.test.js
const request = require('supertest');
const { app, server } = require('../app');
describe('En-têtes de sécurité HTTP', () => {
let response;
beforeAll(async () => {
process.env.NODE_ENV = 'production';
response = await request(app).get('/api/health');
});
afterAll(() => server.close());
test('Content-Security-Policy présent et strict', () => {
const csp = response.headers['content-security-policy'];
expect(csp).toBeDefined();
expect(csp).toContain("default-src 'self'");
expect(csp).not.toContain("'unsafe-eval'");
});
test('X-Frame-Options: DENY', () => {
expect(response.headers['x-frame-options']).toBe('DENY');
});
test('X-Content-Type-Options: nosniff', () => {
expect(response.headers['x-content-type-options']).toBe('nosniff');
});
test('HSTS avec max-age 1 an minimum', () => {
const hsts = response.headers['strict-transport-security'];
expect(hsts).toContain('max-age=31536000');
expect(hsts).toContain('includeSubDomains');
});
test('Referrer-Policy configuré', () => {
expect(response.headers['referrer-policy']).toBe('strict-origin-when-cross-origin');
});
test('X-Powered-By absent', () => {
expect(response.headers['x-powered-by']).toBeUndefined();
});
test('Permissions-Policy présent', () => {
const pp = response.headers['permissions-policy'];
expect(pp).toContain('camera=()');
expect(pp).toContain('microphone=()');
});
test('Cross-Origin-Opener-Policy configuré', () => {
expect(response.headers['cross-origin-opener-policy']).toBe('same-origin');
});
});
NODE_ENV=production npx jest tests/security-headers.test.js --verbose
Sortie attendue avec tous les tests passants :
PASS tests/security-headers.test.js
En-têtes de sécurité HTTP
✓ Content-Security-Policy présent et strict (8ms)
✓ X-Frame-Options: DENY (1ms)
✓ X-Content-Type-Options: nosniff (1ms)
✓ HSTS avec max-age 1 an minimum (1ms)
✓ Referrer-Policy configuré (1ms)
✓ X-Powered-By absent (1ms)
✓ Permissions-Policy présent (1ms)
✓ Cross-Origin-Opener-Policy configuré (1ms)
Test Suites: 1 passed, 1 total
Tests: 8 passed, 8 total
Time: 1.234s
5 pièges courants et comment les éviter
Ces erreurs apparaissent dans la majorité des projets Node.js que nous avons audités. Chacune peut annuler l’effet des en-têtes de sécurité même quand Helmet est correctement installé.
Piège 1 : Placer Helmet après les routes
Helmet doit impérativement être enregistré avant toutes les routes. S’il est placé après, les réponses générées par ces routes ne contiendront pas les en-têtes de sécurité.
// INCORRECT : Helmet après les routes - les routes ci-dessus ne sont pas sécurisées
app.get('/', (req, res) => res.send('Hello, monde!'));
app.use(helmet()); // Trop tard, ne couvre pas les routes définies avant
// CORRECT : Helmet avant toutes les routes
app.use(helmet());
app.get('/', (req, res) => res.send('Hello, monde!'));
Piège 2 : CSP avec ‘unsafe-inline’ dans script-src
La valeur 'unsafe-inline' dans script-src annule entièrement la protection XSS du CSP. Elle permet l’exécution de tout script inline, précisément ce que le CSP est censé bloquer. Un attaquant qui réussit à injecter du HTML peut exécuter n’importe quel script.
// DANGEREUX : annule toute protection XSS
scriptSrc: ["'self'", "'unsafe-inline'"], // Ne jamais utiliser en production
// CORRECT : utiliser des nonces générés dynamiquement
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
// Dans Helmet, utiliser le nonce généré
app.use((req, res, next) => {
helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
},
},
})(req, res, next);
});
// Dans votre template HTML :
// <script nonce="<%= nonce %>">/* code inline sécurisé */</script>
Piège 3 : Activer HSTS sur un serveur sans HTTPS valide
Si vous activez HSTS sur un serveur sans certificat SSL valide (ou avec un certificat expiré), les navigateurs refuseront toute connexion pendant toute la durée de maxAge, sans possibilité de contournement. L’utilisateur voit ERR_SSL_PROTOCOL_ERROR et ne peut plus accéder au site.
// DANGEREUX si HTTPS non fonctionnel
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }
// APPROCHE GRADUELLE recommandée :
// Semaine 1 : 1 jour, vérification que tout fonctionne
hsts: { maxAge: 86400 },
// Semaine 2 : 1 mois
hsts: { maxAge: 2592000 },
// Mois 2 : 6 mois
hsts: { maxAge: 15768000 },
// Mois 3+ : 1 an + preload
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }
Piège 4 : CORS wildcard avec credentials
Un CORS configuré avec origin: '*' et credentials: true est rejeté par la spécification CORS et par les navigateurs modernes. Mais des configurations partiellement incorrectes peuvent créer des brèches de sécurité réelles.
const cors = require('cors');
// INCORRECT : incompatible et potentiellement dangereux
app.use(cors({ origin: '*', credentials: true }));
// CORRECT : liste explicite et restrictive des origines
const allowedOrigins = [
'https://monapp.com',
'https://www.monapp.com',
process.env.NODE_ENV !== 'production' ? 'http://localhost:3000' : null,
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origine CORS non autorisée: ${origin}`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
Piège 5 : COEP qui bloque les ressources CDN
Cross-Origin-Embedder-Policy: require-corp exige que toutes les ressources tierces renvoient l’en-tête Cross-Origin-Resource-Policy. La plupart des CDN ne le font pas encore. Résultat : images, scripts et styles CDN sont bloqués silencieusement, cassant l’apparence ou le fonctionnement de votre application.
// Si vous utilisez des ressources CDN : désactiver COEP temporairement
app.use(
helmet({
crossOriginEmbedderPolicy: false, // Désactivé jusqu'à ce que le CDN supporte CORP
// Ou utiliser 'unsafe-none' :
// crossOriginEmbedderPolicy: { policy: 'unsafe-none' },
})
);
Guide de dépannage : 8 problèmes fréquents
| Problème | Symptôme visible | Solution |
|---|---|---|
| Polices Google Fonts bloquées | Console: “Refused to load font from ‘https://fonts.gstatic.com'” | Ajouter 'https://fonts.gstatic.com' à fontSrc et 'https://fonts.googleapis.com' à styleSrc |
| Scripts CDN (jQuery, Bootstrap) bloqués | Console: “Refused to load script from cdn.jsdelivr.net” | Ajouter le domaine CDN à scriptSrc |
| Images hébergées en externe absentes | Images remplacées par carrés vides, erreur CSP console | Ajouter le domaine d’hébergement à imgSrc |
| Appels API cross-domain bloqués | fetch() échoue avec erreur CORS ou CSP | Ajouter le domaine API à connectSrc |
| iframe partenaire (YouTube, Calendly) bloquée | iframe vide, erreur X-Frame-Options dans console | Utiliser frame-ancestors CSP pour autoriser l’origine spécifique |
| HSTS bloque l’accès HTTP en développement | ERR_SSL_PROTOCOL_ERROR sur localhost | Désactiver hsts en développement : hsts: false |
| Stripe.js ou PayPal SDK bloqué | Formulaire de paiement non chargé | Ajouter 'https://js.stripe.com' à scriptSrc et frameSrc |
| Google Analytics bloqué | Pas de données analytics, erreur CSP | Ajouter 'https://www.googletagmanager.com' à scriptSrc et connectSrc |
Pour les intégrations tierces complexes (Stripe, Intercom, Crisp, HubSpot, etc.), la méthode la plus fiable est :
- Activer CSP en mode
reportOnly: true - Observer les violations pendant 2 à 3 jours en conditions réelles
- Ajouter les domaines violateurs légitimes dans les directives appropriées
- Vérifier qu’il ne reste que des violations d’origines réellement externes et non sollicitées
- Passer en mode enforcement (
reportOnly: false) - Continuer à surveiller l’endpoint
/csp-violationsen production
Conseils avancés pour la sécurité en production
Nonces CSP dynamiques par requête. Générez un nonce unique pour chaque requête HTTP et injectez-le dans vos scripts inline et templates HTML. C’est la seule méthode permettant des scripts inline sans 'unsafe-inline' :
const crypto = require('crypto');
// Middleware nonce : doit être avant Helmet
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
// Helmet utilise le nonce de la requête courante
app.use((req, res, next) => {
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
},
},
})(req, res, next);
});
// Dans EJS :
// <script nonce="<%= nonce %>">alert('script sécurisé');</script>
// Dans Pug :
// script(nonce=nonce) alert('script sécurisé');
En-têtes différenciés par route selon la sensibilité. Toutes les routes n’exigent pas le même niveau de protection. Appliquez une politique plus stricte sur les zones sensibles :
// Sécurité standard pour le dashboard
const standardHelmet = helmet({ frameguard: { action: 'sameorigin' } });
// Haute sécurité pour les zones critiques
const criticalHelmet = [
helmet({ frameguard: { action: 'deny' } }),
(req, res, next) => {
// Empêcher la mise en cache des pages sensibles
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
},
];
app.get('/dashboard', standardHelmet, dashboardController);
app.get('/login', criticalHelmet, loginController);
app.post('/transfer', criticalHelmet, transferController);
app.get('/admin', criticalHelmet, adminController);
Script de vérification CI/CD. Automatisez le contrôle des en-têtes à chaque déploiement pour éviter les régressions :
#!/bin/bash
# scripts/check-security-headers.sh
# Usage : ./check-security-headers.sh https://votre-domaine.com
URL="${1:-https://votre-domaine.com}"
REQUIRED=(
"content-security-policy"
"strict-transport-security"
"x-frame-options"
"x-content-type-options"
"referrer-policy"
"permissions-policy"
"cross-origin-opener-policy"
)
FORBIDDEN=("x-powered-by" "x-aspnet-version")
echo "Vérification des en-têtes de sécurité pour $URL"
HEADERS=$(curl -sI --max-time 10 "$URL" | tr '[:upper:]' '[:lower:]')
FAILED=0
for h in "${REQUIRED[@]}"; do
if echo "$HEADERS" | grep -q "^$h:"; then
echo "OK $h"
else
echo "ERR MANQUANT: $h"
FAILED=1
fi
done
for h in "${FORBIDDEN[@]}"; do
if echo "$HEADERS" | grep -q "^$h:"; then
echo "ERR RÉVÉLATEUR DÉTECTÉ: $h"
FAILED=1
fi
done
[ "$FAILED" -eq 0 ] && echo "SUCCÈS: Configuration sécurisée" && exit 0
echo "ECHEC: Corrigez les problèmes ci-dessus" && exit 1
Pour aller plus loin dans la sécurisation de vos APIs, consultez notre guide OWASP Top 10 Node.js : 12 étapes qui couvre les 10 vulnérabilités critiques au-delà des en-têtes, et notre tutoriel OAuth2 en Node.js : 12 étapes pour l’authentification sécurisée. La gestion sécurisée des mots de passe est traitée dans notre guide bcrypt en Node.js.
Projet complet : structure de fichiers
Voici la structure complète du projet avec tous les fichiers évoqués dans ce tutoriel :
node-security-headers/
├── app.js # Point d'entrée de l'application
├── config/
│ └── helmet.config.js # Configuration Helmet centralisée dev/prod
├── middleware/
│ ├── security.js # Middlewares de sécurité personnalisés
│ ├── csp-nonce.js # Génération des nonces CSP
│ └── permissions-policy.js # En-tête Permissions-Policy
├── routes/
│ ├── api.js # Routes API protégées
│ └── csp-report.js # Endpoint de rapport de violations CSP
├── tests/
│ └── security-headers.test.js # Tests automatisés des en-têtes
├── scripts/
│ └── check-security-headers.sh # Script de vérification CI/CD
├── .env.example # Variables d'environnement requises
└── package.json
# .env.example
NODE_ENV=production
PORT=3000
CSP_REPORT_URI=/csp-violations
ALLOWED_ORIGINS=https://monapp.com,https://www.monapp.com
L’intégralité du code source est accessible sur le dépôt officiel de Helmet.js pour référence sur les options disponibles par version.
Couverture connexe
Pour compléter la sécurisation de vos applications Node.js, ces ressources couvrent les aspects complémentaires aux en-têtes HTTP :
- OWASP Top 10 Node.js : Sécurisez votre API en 12 étapes – les 10 vulnérabilités les plus critiques selon l’OWASP
- Authentification JWT en Node.js : 12 étapes – sécuriser l’authentification avec JSON Web Tokens
- OAuth2 en Node.js : 12 étapes, 30 min – délégation d’autorisation et intégration tierce
- OpenSSL : clés et certificats en 12 étapes – infrastructure à clé publique et TLS
- TLS 1.3 vs TLS 1.2 : 40% plus rapide, 5 CVE – choisir la bonne version TLS pour votre serveur
- bcrypt en Node.js : hachage de mots de passe – stocker les mots de passe de manière sécurisée
- HMAC-SHA256 en Node.js : 10 étapes – intégrité des messages et signatures légères
FAQ : En-têtes de sécurité HTTP en Node.js
Helmet.js est-il suffisant pour sécuriser une application Node.js ?
Non. Helmet gère les en-têtes HTTP de sécurité, ce qui constitue une couche de défense parmi d’autres. La sécurité complète d’une application Node.js couvre également : l’authentification (JWT, sessions), la validation et l’échappement des entrées utilisateurs, la protection contre les injections (SQL, NoSQL, commandes), la gestion sécurisée des dépendances npm, la configuration du serveur, et la surveillance des logs. Les en-têtes de sécurité sont indispensables mais ne remplacent pas la sécurité applicative.
Helmet ralentit-il les performances de l’application ?
L’impact sur les performances est négligeable. Helmet ne fait qu’ajouter des chaînes de caractères aux en-têtes HTTP des réponses. Il ne réalise aucune opération cryptographique, aucune requête en base de données, et aucun traitement CPU significatif. Le surcoût est inférieur à 0,1 ms par requête sur du matériel standard. Le gain en sécurité est sans commune mesure avec ce coût minimal.
Comment gérer le CSP avec un framework frontend (React, Vue, Angular) ?
Les frameworks frontend modernes compilent généralement le code en bundles statiques sans scripts inline, ce qui facilite la compatibilité avec un CSP strict. Les problèmes courants viennent des polyfills inline injectés par les bundlers. L’approche recommandée est d’utiliser des nonces CSP injectés dans l’HTML lors du rendu (SSR avec Next.js, Nuxt, etc.), ou des hashes CSP pour les scripts inline connus et statiques. Pour les SPA pures (sans SSR), la CSP doit être configurée au niveau du serveur statique (nginx ou Express static).
Quelle est la différence entre X-Frame-Options et frame-ancestors CSP ?
frame-ancestors dans la CSP est la méthode moderne : elle supporte plusieurs origines autorisées simultanément et des patterns wildcards. X-Frame-Options est plus ancien, plus simple, et supporté par des navigateurs très anciens qui ne comprennent pas encore la CSP. Helmet active les deux par défaut pour une compatibilité maximale. En cas de conflit entre les deux en-têtes, frame-ancestors a la priorité dans les navigateurs qui le supportent.
Dois-je activer HSTS si mon application est derrière un proxy inverse ?
HSTS doit être émis par le composant qui gère la connexion TLS avec le client, c’est-à-dire votre proxy inverse (nginx, Apache, Cloudflare) dans la majorité des déploiements en production. Si votre proxy gère le TLS et communique avec Node.js en HTTP interne, configurez HSTS dans nginx et désactivez-le dans Helmet avec hsts: false. Si Node.js gère directement le TLS (rare en production), utilisez Helmet pour HSTS. Évitez d’avoir HSTS configuré aux deux niveaux simultanément.
Comment tester mes en-têtes sans déployer en production ?
Utilisez curl -I http://localhost:3000 pour vérifier les en-têtes en local, supertest pour les tests automatisés dans votre pipeline CI/CD, et ngrok ou Cloudflare Tunnel pour exposer temporairement votre serveur local à des outils d’analyse en ligne. Mozilla Observatory et d’autres scanners nécessitent un domaine public, mais peuvent être utilisés avec des domaines de staging avant chaque mise en production.
Que faire si Helmet casse mon application existante ?
Désactivez d’abord le CSP (contentSecurityPolicy: false) et activez-le progressivement en mode reportOnly: true. Activez ensuite les autres en-têtes un par un en vérifiant les effets. Le coupable le plus fréquent est COEP (crossOriginEmbedderPolicy) qui bloque les ressources CDN tierces. Si votre application embarque beaucoup de ressources externes, désactivez COEP temporairement avec crossOriginEmbedderPolicy: false et réactivez-le progressivement quand vos fournisseurs CDN supportent CORP.
Les en-têtes de sécurité protègent-ils contre les attaques XSS côté serveur ?
Non, pas directement. Le CSP empêche l’exécution de code malveillant dans le navigateur du client, mais une injection réussie dans votre base de données, vos fichiers de template ou vos emails reste possible sans validation serveur. Le CSP est une dernière ligne de défense côté navigateur. Pour une protection complète contre le XSS, combinez : validation des entrées côté serveur, échappement contextuel des sorties dans les templates, et CSP strict. La validation HMAC des données sensibles, traitée dans notre guide HMAC-SHA256 en Node.js, complète cette défense en profondeur.




