Concevoir une architecture sécurisée
La sécurité par conception : défense en profondeur, moindre privilège, authentification/autorisation solides et secrets bien gérés.
Jusqu'ici, nous avons surtout démonté des attaques : XSS, injections, CSRF, falsification de requêtes côté serveur. Comprendre l'offensive était un préalable, mais le but de tout ce travail est ailleurs. Comme l'écrit Andrew Hoffman, défendre une application web ressemble à défendre un château médiéval : un ensemble de murs et de bâtiments — le code applicatif — entouré de dépendances et d'intégrations qui forment le reste du royaume. La surface est vaste, et il serait illusoire de fortifier chaque porte au maximum. Il faut donc concevoir la défense, pas la rajouter après coup.
C'est tout l'objet de ce chapitre : la sécurité par conception (secure by design). Hoffman insiste sur un fait que le NIST a chiffré : corriger une faille de sécurité pendant la phase d'architecture coûte 30 à 60 fois moins cher que la corriger en production. Une fois que des clients s'appuient sur une fonctionnalité bancale, qu'ils ont signé des contrats et bâti leurs processus dessus, une ré-architecture devient un cauchemar. La meilleure phase pour traiter un problème de sécurité est donc toujours la première.
La sécurité se conçoit, elle ne se rajoute pas
L'essentiel du génie logiciel consiste à déplacer efficacement des données d'un point A à un point B. L'essentiel du génie de la sécurité consiste à sécuriser ces données en transit, et partout où elles reposent avant, pendant et après ce transit. Cette grille de lecture — suivre la donnée — est la boussole de la phase d'architecture.
Avant la première ligne de code, on collecte les exigences métier et on les évalue pour le risque. Hoffman prend l'exemple de MegaMerch, une boutique en ligne dont le cahier des charges (création de compte, données personnelles, recherche d'articles, cartes bancaires enregistrées) révèle d'emblée plusieurs zones sensibles : on stocke des identifiants, des informations personnelles (PII), des données financières, et on distingue des privilèges entre invités et membres. Chacun de ces points soulève des questions d'implémentation qui sont autant d'occasions de pousser l'application dans une direction plus sûre.
Note
Toute organisation qui sépare les équipes sécurité et R&D doit intégrer des canaux de communication entre elles dans le processus de développement. Une fonctionnalité ne peut pas être analysée en silo : l'analyse de risque doit réunir ingénierie ET produit, dès la conception.
Une revue de code par engagement (per-commit) prolonge ce travail. Hoffman propose une grille minimale, applicable par n'importe qui, qui revient à pister la donnée : comment est-elle transmise de A à B (réseau, format) ? Comment est-elle stockée ? Comment est-elle présentée à l'utilisateur côté client ? Quelles opérations subit-elle côté serveur avant d'être persistée ?
Les grands principes d'architecture sécurisée
Avant d'entrer dans les briques concrètes (mots de passe, sessions, chiffrement), il faut poser les principes transverses qui guident chaque décision. Ils ne dépendent ni du langage ni du framework.
Défense en profondeur
La défense en profondeur (defense in depth) consiste à empiler plusieurs couches de protection indépendantes, de sorte qu'aucune faille unique ne soit fatale. Le château n'a pas qu'un seul mur : il a des douves, une herse, une enceinte extérieure, un donjon. Si l'assaillant franchit une barrière, il en trouve une autre derrière.
Concrètement, on ne se repose jamais sur un seul contrôle. Un même formulaire peut être protégé par une validation côté client (confort), une validation côté serveur (réelle barrière), des requêtes paramétrées (anti-injection), un encodage en sortie (anti-XSS) et une politique de sécurité de contenu (filet de sécurité). Si l'un cède, les autres tiennent.
Moindre privilège
Le principe de moindre privilège (least privilege) veut que chaque composant, service et utilisateur n'obtienne que les droits strictement nécessaires à sa tâche, et rien de plus. Le compte de base de données utilisé par l'application web n'a pas besoin des droits d'administration du serveur ; un microservice de recherche n'a pas besoin d'écrire dans la table des paiements ; un utilisateur connecté n'a pas besoin de lire les commandes d'autrui.
L'intérêt défensif est direct : si un composant est compromis, les dégâts sont bornés par les privilèges minuscules qu'on lui a accordés. Une injection SQL via un compte en lecture seule sur une seule table fait infiniment moins de dégâts que via un compte root.
Ne jamais faire confiance aux entrées
Toute donnée venant de l'extérieur — corps de requête, en-têtes, paramètres d'URL, fichiers, voire réponses d'API tierces — est potentiellement hostile. On la valide et on l'assainit à la frontière du système, avant qu'elle ne touche la logique métier, la base de données ou le navigateur d'un autre utilisateur. Ce principe sous-tend toutes les défenses contre l'injection et le XSS vues dans les chapitres précédents.
Réduire la surface d'attaque
Chaque point d'entrée, chaque dépendance, chaque endpoint exposé est une porte potentielle. Réduire la surface d'attaque (attack surface), c'est désactiver ce dont on n'a pas besoin : routes de débogage en production, comptes par défaut, ports ouverts inutilement, fonctionnalités héritées, dépendances superflues. Moins il y a de portes, moins il y a de portes à garder.
Sécurisé par défaut et échec sécurisé
Une bonne architecture est sécurisée par défaut (secure defaults) : la configuration de base est la plus stricte, et c'est l'ouverture qui demande un acte explicite, jamais le verrouillage. Corollaire essentiel : en cas d'erreur, le système doit échouer de façon sécurisée (fail securely) — c'est-à-dire refuser l'accès, pas l'accorder.
// ❌ Avant : en cas d'erreur, on laisse passer (fail open).
async function peutAcceder(user, ressource) {
try {
return await verifierDroits(user, ressource);
} catch (e) {
return true; // l'erreur ouvre la porte !
}
}
// ✅ Après : toute incertitude refuse l'accès (fail closed).
async function peutAcceder(user, ressource) {
try {
return await verifierDroits(user, ressource);
} catch (e) {
journal.error("Echec verif droits", e);
return false; // par défaut, on refuse
}
} À retenir
Le principe « fail securely » est l'un des plus violés en pratique. Un catch qui retourne true, une condition d'autorisation par défaut permissive, un timeout interprété comme un succès : autant de portes ouvertes par accident. La règle est invariable — dans le doute, on refuse.
| Principe | Application concrète |
|---|---|
| Défense en profondeur | Empiler validation serveur + requêtes paramétrées + encodage + CSP |
| Moindre privilège | Compte BDD restreint par service ; rôles utilisateurs étroits |
| Ne pas faire confiance aux entrées | Valider/assainir à la frontière, côté serveur |
| Réduire la surface d'attaque | Désactiver routes de debug, comptes et ports inutiles |
| Sécurisé par défaut / fail securely | Config stricte par défaut ; toute erreur refuse l'accès |
Protéger la donnée en transit : TLS partout
La première décision d'architecture que dicte l'analyse de MegaMerch concerne la donnée en transit, car elle conditionne le flux de toutes les données. L'exigence initiale est simple : tout ce qui circule sur le réseau doit être chiffré en route. Cela réduit le risque d'attaque de l'intercepteur (man-in-the-middle), qui pourrait voler des identifiants et effectuer des achats au nom de l'utilisateur.
Deux protocoles cryptographiques dominent pour cela : SSL (Secure Sockets Layer), conçu par Netscape au milieu des années 1990, et TLS (Transport Layer Security), défini par la RFC 2246 en 1999, qui corrige plusieurs faiblesses architecturales de SSL. TLS offre la sécurité la plus rigide ; SSL est plus répandu mais souffre de vulnérabilités qui entament son intégrité. Le schéma d'URI HTTPS (HTTP Secure) impose la présence de TLS/SSL avant tout échange, et les navigateurs affichent le cadenas — ou un avertissement si la connexion est compromise.
L'implémentation est spécifique au serveur, mais tous les serveurs web majeurs offrent une intégration simple pour chiffrer le trafic. Pour MegaMerch, l'objectif est que toute donnée soit chiffrée et compatible TLS avant d'être envoyée. Des autorités comme Let's Encrypt fournissent gratuitement les certificats nécessaires.
Attention
Servir le chiffrement n'est pas suffisant si l'application accepte aussi le trafic en clair. Sans redirection forcée et sans en-tête HSTS (voir plus bas), un attaquant peut tenter une attaque de « rétrogradation » (SSL stripping) qui pousse la victime vers une connexion HTTP non chiffrée. HTTPS doit être imposé, pas simplement disponible.
Une authentification robuste
Parce que MegaMerch stocke des identifiants et offre une expérience différente aux invités et aux membres, l'application possède forcément un système d'authentification (prouver qui l'on est) et un système d'autorisation (déterminer ce que l'on a le droit de faire). Commençons par la première.
Mots de passe : entropie plutôt que complexité cosmétique
La plupart des développeurs croient qu'un mot de passe sûr tient à sa longueur et à ses caractères spéciaux. En réalité, ce qui compte est l'entropie — la quantité de hasard et d'imprévisibilité. Les attaques par dictionnaire exploitent les motifs : listes des mots de passe les plus courants, structures fréquentes, combinaisons connues. Un mot de passe long mais prévisible reste faible.
Comme il est difficile de transmettre cette idée aux utilisateurs, Hoffman recommande de leur rendre difficile la création d'un mot de passe à motifs connus : rejeter les mots de passe figurant dans une liste des mille plus courants, et interdire les éléments dérivés des données de l'utilisateur — prénom, nom, date de naissance, fragments d'adresse. MegaMerch collectant ces champs à l'inscription, il peut précisément les bannir du mot de passe.
Hacher, jamais stocker en clair
La règle est absolue : on ne stocke jamais un mot de passe en clair. On le hache dès qu'on le voit, avant de le stocker. Le hachage diffère du chiffrement sur un point décisif : il est irréversible. On ne veut pas que même notre propre personnel puisse récupérer les mots de passe — les utilisateurs les réutilisent ailleurs, et c'est une responsabilité qu'on refuse d'endosser en cas d'employé malveillant.
Hoffman illustre par trois scénarios de fuite de la base MegaMerch :
| Stockage | Conséquence d'une fuite |
|---|---|
| Mots de passe en clair | Tous compromis immédiatement |
| Hachés en MD5 | Certains cassés via tables arc-en-ciel (rainbow tables) |
| Hachés en bcrypt | Très improbable qu'un seul soit cassé |
La leçon : tout mot de passe doit être haché, et l'algorithme doit être choisi pour son intégrité mathématique et sa lenteur sur le matériel moderne. Un algorithme lent réduit le nombre d'essais par seconde qu'un attaquant peut tenter.
// ❌ Avant : hachage rapide, non salé, cassable.
const crypto = require("crypto");
function stockerMotDePasse(motDePasse) {
// MD5/SHA1 nus : rapides, vulnérables aux rainbow tables
const hash = crypto.createHash("md5").update(motDePasse).digest("hex");
return db.users.save({ hash });
}
// ✅ Après : bcrypt, lent et salé automatiquement.
const bcrypt = require("bcrypt");
const COUT = 12; // facteur de coût, à augmenter avec le matériel
async function stockerMotDePasse(motDePasse) {
const hash = await bcrypt.hash(motDePasse, COUT);
return db.users.save({ hash });
}
async function verifierMotDePasse(motDePasse, hashStocke) {
return bcrypt.compare(motDePasse, hashStocke);
} bcrypt tire son nom du chiffrement Blowfish et de la fonction crypt historique d'Unix. Son atout : il devient plus lent sur du matériel plus rapide, donc il « passe à l'échelle vers le futur » — plus la machine de l'attaquant est puissante, plus chaque essai coûte cher. Hoffman cite aussi PBKDF2, fondé sur l'étirement de clé (key stretching) : on configure un nombre minimal d'itérations, qu'on règle au maximum que le matériel peut supporter. Aujourd'hui, on ajoute volontiers argon2 à cette famille d'algorithmes adaptés. Pour MegaMerch, le choix retenu est bcrypt, et l'on ne compare jamais que des hachés.
Piège courant
N'utilisez jamais MD5, SHA-1 ou un SHA-256 « nu » pour stocker des mots de passe : ces fonctions sont conçues pour être rapides, exactement le contraire de ce qu'il faut ici. Un hachage sans sel (salt) expose en plus aux tables arc-en-ciel. Utilisez une fonction dédiée — bcrypt, argon2, PBKDF2 — qui sale et ralentit par construction.
Le second facteur (2FA / MFA)
Au-delà du mot de passe haché et chiffré en transit, MegaMerch devrait proposer le 2FA (authentification à deux facteurs, généralisée en MFA, multi-factor authentication). Le principe est simple : en plus du mot de passe saisi dans le navigateur, l'utilisateur fournit un code généré par une application mobile (type Google Authenticator) ou un SMS, voire un jeton matériel USB pour les usages les plus sensibles (plutôt réservés aux employés qu'aux clients).
L'apport défensif est considérable : en l'absence de vulnérabilité dans l'application 2FA ou le protocole de messagerie, le 2FA élimine les connexions distantes non initiées par le propriétaire du compte. Compromettre un compte protégé exige désormais d'obtenir à la fois le mot de passe et l'appareil physique portant les codes. Même le 2FA par SMS, moins robuste qu'un jeton dédié, reste d'un ordre de grandeur plus sûr qu'une absence totale de second facteur.
Sessions sûres
Une fois l'utilisateur authentifié, la session qui matérialise sa connexion doit être protégée. Le cookie de session doit porter les attributs HttpOnly (inaccessible au JavaScript, donc à l'abri du vol par XSS), Secure (transmis uniquement sur HTTPS) et SameSite (rempart contre le CSRF). Il doit expirer, et l'identifiant doit être renouvelé après une élévation de privilège comme la connexion (rotation, contre la fixation de session).
Set-Cookie: session=a1b2c3...; HttpOnly; Secure; SameSite=Strict;
Path=/; Max-Age=3600 L'autorisation : vérifier côté serveur, à chaque requête
L'authentification dit qui est l'utilisateur ; l'autorisation dit ce qu'il a le droit de faire. C'est ici que se logent certaines des failles les plus courantes — et les plus dévastatrices.
La règle cardinale : vérifier les droits à chaque requête, côté serveur, et ne jamais se fier au client. Masquer un bouton ou une entrée de menu dans l'interface n'est pas un contrôle d'accès : c'est du confort visuel. L'attaquant n'utilise pas votre interface — il forge directement la requête HTTP. Un menu caché ne protège rien.
// ❌ Avant : aucune vérification serveur, on fait confiance à l'URL.
app.get("/api/commandes/:id", async (req, res) => {
const commande = await db.commandes.findById(req.params.id);
res.json(commande); // n'importe qui lit la commande de n'importe qui
}); Ce code illustre une référence directe non sécurisée à un objet (Insecure Direct Object Reference, IDOR) : en incrémentant l'id dans l'URL, un utilisateur accède aux commandes d'autrui. La correction consiste à vérifier, côté serveur, que la ressource demandée appartient bien à l'appelant — et à échouer de façon fermée sinon.
// ✅ Après : autorisation vérifiée côté serveur, à chaque requête.
app.get("/api/commandes/:id", exigerAuth, async (req, res) => {
const commande = await db.commandes.findById(req.params.id);
if (!commande) return res.status(404).end();
// l'objet doit appartenir à l'utilisateur authentifié
if (commande.userId !== req.user.id && !req.user.estAdmin) {
return res.status(403).end(); // fail closed
}
res.json(commande);
}); Pour rester cohérent, ce contrôle doit être centralisé plutôt que recopié dans chaque route — une couche d'autorisation unique (intergiciel, politique, service de décision) qu'on traverse systématiquement. Disperser la logique d'accès, c'est garantir qu'on l'oubliera quelque part.
Attention
Renvoyer 404 plutôt que 403 quand une ressource existe mais n'appartient pas à l'appelant évite en plus de divulguer l'existence de l'objet (énumération). Choisissez votre stratégie, mais ne révélez jamais qu'un identifiant valide existe à quelqu'un qui n'a pas le droit de le voir.
| Menace / vecteur | Contre-mesure |
|---|---|
| Contrôle d'accès uniquement côté client | Vérifier les droits côté serveur à chaque requête |
| IDOR (accès à l'objet d'autrui) | Vérifier la propriété de la ressource avant de répondre |
| Logique d'autorisation dispersée | Couche d'autorisation centralisée et systématique |
| Énumération de ressources | Répondre 404/403 sans divulguer l'existence |
Chiffrer les données au repos et gérer les secrets
La protection en transit (TLS) ne suffit pas : les PII (personally identifiable information) — nom complet, adresse, date de naissance — et les données financières comme les numéros de carte doivent aussi être protégées au repos. Hoffman rappelle deux exigences : s'assurer que ce stockage est légal dans les pays d'opération et conforme aux lois applicables (les données financières relèvent des lois sur les PII dans certains pays) ; et garantir qu'en cas de fuite, ces données ne soient pas exploitables facilement.
Pour une petite structure, une stratégie souvent plus sûre que de stocker ces données soi-même consiste à externaliser leur conservation vers un prestataire spécialisé et conforme — qui porte alors la charge réglementaire et technique du stockage sécurisé.
Quant aux secrets de l'application (clés d'API, identifiants de base de données, clés de chiffrement), ils ne doivent jamais figurer dans le code source ni dans le dépôt de versions. On les injecte par des variables d'environnement ou, mieux, par un coffre-fort à secrets (secrets vault) dédié.
// ❌ Avant : secret en dur, versionné dans le dépôt.
const stripe = require("stripe")("sk_live_a1b2c3d4e5f6...");
// ✅ Après : secret injecté hors du code source.
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); Piège courant
Un secret commité une fois reste dans l'historique Git même après suppression. Si une clé fuit, il faut la considérer compromise et la révoquer/rotater immédiatement — pas seulement la retirer du code. Ajoutez les fichiers de configuration sensibles à .gitignore avant le premier commit.
En-têtes de sécurité, journalisation et surveillance
Les en-têtes de réponse de sécurité forment une couche de défense en profondeur que le navigateur applique pour vous. Les principaux :
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer Strict-Transport-Security (HSTS) force le navigateur à n'utiliser que HTTPS, fermant la porte au SSL stripping. La politique de sécurité de contenu (Content-Security-Policy, CSP) limite les origines de scripts et constitue un filet contre le XSS. X-Content-Type-Options: nosniff empêche le navigateur de « deviner » un type MIME, et X-Frame-Options bloque l'inclusion en iframe (clickjacking).
Enfin, journaliser et surveiller est indispensable. Hoffman souligne un point précis : lorsqu'une vulnérabilité connue attend son correctif, il faut ajouter de la journalisation pour s'assurer qu'aucun attaquant ne l'exploite pendant l'attente. Plusieurs entreprises ont sombré faute d'avoir su qu'une faille était activement abusée. Une bonne journalisation — accès, échecs d'authentification, refus d'autorisation, erreurs — et une surveillance qui en tire l'alerte permettent de détecter ce qu'on n'a pas pu prévenir.
Attention
La journalisation est à double tranchant : ne consignez jamais de mots de passe, de jetons de session, de numéros de carte ou de PII en clair dans les logs. Un fichier de logs trop bavard devient lui-même une cible de fuite. Journalisez les événements, pas les secrets.
Intégrer la sécurité dans tout le cycle (DevSecOps)
La sécurité n'est pas une étape isolée : elle traverse tout le cycle de développement, de l'architecture à la maintenance. Hoffman décrit ce continuum : analyse de risque des exigences, revues de code orientées sécurité (idéalement par une équipe distincte, pour éviter les conflits d'intérêt), découverte de vulnérabilités (programmes de bug bounty, équipes red/blue, tests d'intrusion autorisés, incitations internes à signaler), puis triage, priorisation, suivi et tests de non-régression après chaque correctif — car une part importante des vulnérabilités sont des régressions de bugs déjà fermés.
Cette intégration continue de la sécurité, aujourd'hui appelée DevSecOps, prolonge l'idée maîtresse du chapitre : il est bien moins coûteux de traiter un défaut tôt que de le subir tard. La stratégie d'atténuation (mitigation) doit être à la fois large comme un filet, et profonde dans les zones critiques.
À retenir
La découverte de vulnérabilités doit rester dans un cadre légal et éthique : on ne teste que des systèmes que l'on possède ou pour lesquels on dispose d'une autorisation explicite (programme de bug bounty, mandat de pentest). En cas de découverte sur un système tiers, la voie est la divulgation responsable (responsible disclosure) auprès de l'éditeur — jamais l'exploitation.
À retenir
- La sécurité se conçoit, elle ne se rajoute pas : corriger une faille en phase d'architecture coûte 30 à 60 fois moins qu'en production. Suivez la donnée de A à B.
- Cinq principes transverses : défense en profondeur (couches indépendantes), moindre privilège (droits strictement nécessaires), ne jamais faire confiance aux entrées, réduire la surface d'attaque, et sécurisé par défaut / fail securely (dans le doute, on refuse).
- Authentification robuste : TLS partout, mots de passe hachés avec bcrypt/argon2/PBKDF2 (lents, salés — jamais MD5/SHA nu ni clair), proposez le MFA, sécurisez les sessions (
HttpOnly,Secure,SameSite, expiration, rotation). - Autorisation : vérifiez les droits côté serveur, à chaque requête, de façon centralisée ; un menu caché n'est pas un contrôle d'accès ; vérifiez la propriété des objets (anti-IDOR).
- Chiffrement et secrets : données sensibles chiffrées au repos (ou externalisées chez un prestataire conforme) ; secrets hors du code source, en variables d'environnement ou coffre-fort.
- Couches finales et culture : en-têtes de sécurité (HSTS, CSP,
nosniff…), journalisation/surveillance sans consigner de secrets, et sécurité intégrée à tout le cycle (DevSecOps), dans un cadre légal et de divulgation responsable.