La reconnaissance d'une application web
Ce qu'un attaquant apprend sur votre application avant de frapper — et comment réduire votre surface d'exposition.
Avant qu'une seule charge utile malveillante ne soit envoyée, un attaquant compétent passe l'essentiel de son temps à observer. Il cartographie vos sous-domaines, devine la forme de vos API, identifie vos frameworks et leurs versions, et repère les recoins de votre architecture où la sécurité est la plus faible. Cette phase de collecte d'informations s'appelle la reconnaissance (recon). Hoffman lui consacre toute la première partie de son livre, et pour une bonne raison : on n'attaque bien que ce que l'on connaît bien.
Ce chapitre prend délibérément le contre-pied de l'attaquant. Si la recon consiste à apprendre tout ce qu'une application laisse filtrer, alors la défense consiste à savoir ce que l'on expose pour le réduire. Comprendre les techniques de cartographie n'est pas un but en soi : c'est le moyen de dresser l'inventaire de votre propre surface d'attaque (attack surface) et de la rétrécir méthodiquement. Nous parcourrons donc, dans l'ordre, la structure d'une application web moderne, puis les grandes sources d'information — sous-domaines, API, dépendances tierces, signaux d'architecture — en répondant chaque fois à une seule question : « qu'est-ce que mon application laisse voir, et comment le cacher ? »
Note
Cadre éthique. Toutes les techniques décrites ici ne s'emploient que contre des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation écrite (test d'intrusion mandaté, programme de prime aux bugs avec un périmètre défini). Sonder un système tiers sans accord est illégal dans la plupart des juridictions. L'objectif de ce chapitre est défensif : auditer votre propre exposition et celle de vos clients consentants.
Connaître la structure d'une application moderne
On ne sécurise bien que ce que l'on comprend. Les applications d'aujourd'hui ne ressemblent plus aux pages rendues côté serveur d'il y a une décennie. Une application moderne est souvent plusieurs applications qui communiquent par le réseau : une interface monopage (single-page application, SPA) dans le navigateur, une ou plusieurs API REST, des bases de données côté serveur, et du stockage côté client. Chaque couche ajoute de la fonctionnalité — et autant de surface d'attaque.
REST, JSON et API sans état
Le style architectural dominant est REST (Representational State Transfer). Une API REST est séparée du client, sans état (stateless) — elle ne mémorise rien sur le demandeur entre deux requêtes —, facilement mise en cache, et chaque endpoint désigne une ressource plutôt qu'une fonction. Les verbes HTTP donnent le reste : GET pour lire, POST pour créer, PUT/PATCH pour modifier, DELETE pour supprimer.
Cette régularité est une force pour les développeurs… et un cadeau pour l'attaquant. Une API hiérarchique et auto-documentée se devine. Si l'on observe GET /users/1234, on suppose immédiatement l'existence de GET /users/1234/payments, voire de DELETE /users/1234. Le format d'échange est presque toujours JSON (JavaScript Object Notation) : léger, hiérarchique, lisible, et trivial à analyser dans le navigateur via JSON.parse. Retenez ce point pour la suite : la prévisibilité de REST + JSON facilite la cartographie, donc la défense ne peut pas reposer sur l'obscurité de la structure, mais sur l'autorisation de chaque endpoint.
Les particularités de JavaScript qui comptent pour la sécurité
JavaScript est le seul langage de script côté client dans le navigateur. Hoffman insiste sur cinq concepts qu'il faut maîtriser pour comprendre les failles : la portée (scope), le contexte (context), l'héritage prototypal (prototypal inheritance), l'asynchronisme (asynchrony) et le DOM (Document Object Model) du navigateur.
Deux d'entre eux ont des conséquences directes sur la sécurité. D'abord, toute variable déclarée sans var, let ou const est hissée dans la portée globale et attachée à l'objet window — source de conflits de nommage et de bugs exploitables.
// VULNÉRABLE : age fuite dans la portée globale (window.age).
function setAge() {
age = 25; // pas de mot-clé : pollution du global
}
// CORRIGÉ : portée de bloc, rien ne fuit sur window.
function setAge() {
const age = 25;
return age;
} Ensuite, l'héritage prototypal : tout objet JavaScript possède un prototype modifiable à l'exécution, dont les changements se propagent à tous les enfants. C'est précisément ce qui rend possible la pollution de prototype (prototype pollution), une famille d'attaques où la modification d'un objet parent altère le comportement de tous les objets qui en héritent. Nous y reviendrons dans un chapitre dédié ; à ce stade, sachez que cette mécanique existe et qu'elle façonne le modèle de menace côté client.
Serveurs, bases et stockage client
Côté serveur, la logique applicative tourne sur un logiciel de serveur web (Apache, Nginx, Microsoft IIS) posé sur un système d'exploitation, généralement Linux. Les données persistent dans des bases côté serveur : SQL (PostgreSQL, MySQL, SQL Server, SQLite) ou NoSQL (MongoDB, CouchDB), souvent plusieurs à la fois. Hoffman souligne un piège récurrent : une application dont la génération de requêtes SQL est blindée peut très bien avoir des requêtes MongoDB ou Elasticsearch insuffisamment sécurisées. Chaque base introduit son propre modèle de requête, donc son propre risque d'injection.
Côté client, le navigateur offre le stockage local (local storage), le stockage de session (session storage) et IndexedDB, tous soumis à la politique de même origine (Same Origin Policy, SOP). Un point de vigilance immédiat :
Piège courant
Ne stockez jamais de secrets dans le stockage client. Dans les applications mal conçues, le stockage local ou de session révèle des jetons d'authentification, des clés d'API ou d'autres secrets — accessibles à tout script s'exécutant dans la page (donc à toute XSS). Le stockage client est lisible par l'utilisateur et par le JavaScript de la page : traitez-le comme public.
Authentification et autorisation
Deux notions à ne jamais confondre. L'authentification (authentication) répond à « qui êtes-vous ? » : elle prouve que « joe123 » est bien « joe123 ». L'autorisation (authorization) répond à « avez-vous le droit ? » : elle détermine que « joe123 » peut voir ses propres photos privées, mais pas celles de « susan1988 ».
Hoffman donne ici un conseil défensif essentiel : centralisez l'autorisation. Les applications bien conçues possèdent une classe d'autorisation unique qui décide de l'accès aux ressources. Les applications fragiles réimplémentent les contrôles API par API, ce qui multiplie les occasions d'erreur humaine — il suffit d'un endpoint où le contrôle est oublié.
// VULNÉRABLE : le contrôle d'accès est recopié dans chaque endpoint.
app.get('/photos/:id', (req, res) => {
const photo = db.findPhoto(req.params.id);
// l'oubli de cette ligne sur un seul endpoint = fuite de données
if (photo.ownerId !== req.user.id) return res.sendStatus(403);
res.json(photo);
});
// CORRIGÉ : une autorité centrale, appelée partout de façon uniforme.
app.get('/photos/:id', requireOwnership('photo'), (req, res) => {
res.json(req.resource);
}); À retenir
Toujours protéger par un contrôle d'autorisation, selon Hoffman : la mise à jour du profil et des paramètres, la réinitialisation de mot de passe, la lecture et l'écriture de messages privés, toute fonctionnalité payante, et toute fonction réservée aux utilisateurs élevés (modération, administration). Un endpoint d'admin que l'interface n'affiche jamais reste atteignable directement : il doit vérifier les permissions comme tous les autres.
Découverte de sous-domaines : ce que votre DNS révèle
Il est rare qu'une application tienne sur un seul domaine. Derrière www.mega-bank.com se cachent souvent mail., admin., dev., internal., ftp.… Et voilà le point névralgique défensif : le domaine grand public est scruté en permanence et corrigé vite ; les serveurs « en coulisses » sont bien plus bogués car peu exposés. Découvrir admin.mega-bank.com peut offrir à un attaquant un raccourci précieux.
Du point de vue de l'attaquant, plusieurs sources livrent vos sous-domaines. Du point de vue défensif, chacune est une fuite à colmater.
| Source d'information | Ce qu'elle révèle | Contre-mesure |
|---|---|---|
| Outils réseau du navigateur | Domaines d'API appelés en arrière-plan | Limiter les domaines exposés au client ; segmenter le réseau |
Moteurs de recherche (site:, -inurl:) | Sous-domaines indexés | Robots.txt, noindex, retirer du web public ce qui doit rester interne |
| Archives (archive.org) | Sous-domaines et liens anciens, retirés du site vif | Considérer toute fuite comme permanente ; faire tourner les secrets exposés |
| Réseaux sociaux (API publiques) | Sous-domaines promus en campagnes, recrutement | Inventorier les domaines marketing ; revue de sécurité identique au cœur applicatif |
| Transfert de zone DNS | Toute la liste des sous-domaines et leurs IP | Restreindre les transferts de zone aux seuls serveurs esclaves autorisés |
| Brute force / dictionnaire | Sous-domaines au nom commun (dev, test, admin) | Journalisation, limitation de débit, isolement réseau des hôtes internes |
Le transfert de zone : la fuite à fermer en priorité
Un serveur DNS traduit les noms lisibles (mega-bank.com) en adresses IP. Pour rester synchronisés, les serveurs DNS s'échangent leurs enregistrements via un transfert de zone (zone transfer), au format texte appelé fichier de zone. Le problème : un serveur maître mal configuré peut répondre à une demande de transfert venant de n'importe qui, et livrer d'un coup l'inventaire complet de vos sous-domaines internes avec leurs IP. Ce n'est même pas un « hack » : c'est une fonction légitime du protocole, détournée par une mauvaise configuration.
La défense est simple et tient en une règle :
Attention
Un serveur DNS maître ne doit jamais accepter une demande de transfert de zone d'un hôte arbitraire. Configurez-le pour n'autoriser le transfert qu'à une liste blanche de serveurs esclaves explicitement définis. Un serveur correctement configuré répond Transfer Failed. à toute autre demande. Vérifiez cette configuration : c'est l'une des fuites les plus faciles à fermer et l'une des plus rentables pour un attaquant.
Brute force et dictionnaire : ce que vous pouvez détecter
Quand les méthodes discrètes échouent, l'attaquant tente toutes les combinaisons (brute force) ou parcourt une liste des sous-domaines les plus courants (attaque par dictionnaire — www, mail, ftp, webmail, admin…). Bonne nouvelle pour le défenseur : ces tentatives sont bruyantes. Elles génèrent un volume anormal de requêtes DNS ou HTTP, faciles à journaliser et à bloquer.
Astuce
Les attaques par force brute sont faciles à détecter : elles laissent des traces dans vos journaux et peuvent faire bannir l'IP de l'attaquant. Côté défense, mettez en place une limitation de débit (rate limiting), une surveillance des pics de requêtes sur des noms inexistants, et — surtout — n'exposez pas sur le réseau public les hôtes qui n'ont rien à y faire (dev, staging, internal). La meilleure protection contre la découverte d'un sous-domaine interne, c'est qu'il soit confiné à un réseau interne.
Analyse d'API : minimiser ce que vos endpoints racontent
Une fois les sous-domaines connus, l'étape suivante est l'analyse d'API : découvrir les endpoints, deviner les verbes acceptés, comprendre la forme des requêtes/réponses et le mécanisme d'authentification.
Découverte d'endpoints et de verbes
Le plus simple, pour un attaquant, est d'observer le trafic réel via les outils réseau du navigateur, puis d'extrapoler. La méthode OPTIONS du protocole HTTP est censée annoncer les verbes supportés par un endpoint — mais peu d'applications d'entreprise l'exposent, justement par prudence. À défaut, on essaie chaque verbe (GET, POST, PUT, PATCH, DELETE) sur une ressource connue pour voir lesquels répondent.
Attention
Essayer aveuglément les verbes HTTP sur une API peut supprimer ou altérer des données : un DELETE ou un PUT mal placé n'est pas anodin. C'est pourquoi ces tentatives ne se font qu'avec l'autorisation explicite du propriétaire de l'application. Côté défense, n'implémentez sur chaque ressource que les verbes strictement nécessaires, et répondez 405 Method Not Allowed (sans détail) aux autres.
Forme des charges utiles et messages d'erreur
C'est ici que la leçon défensive devient concrète. Pour deviner la forme (shape) attendue par un endpoint, l'attaquant s'appuie d'abord sur les spécifications publiques (un flux OAuth 2.0 a une forme standard et documentée). Pour les endpoints spécifiques à l'application, il procède par essais-erreurs — et vos messages d'erreur sont sa meilleure aide.
# VULNÉRABLE : le serveur dicte à l'attaquant le champ manquant,
# puis les valeurs valides du champ.
HTTP/1.1 400 Bad Request
{ "error": "auth_token not supplied" }
# puis, après ajout du token :
{ "error": "publicProfile only accepts "auth" and "noAuth"" } Un message aussi bavard transforme la devinette en simple lecture : l'application décrit elle-même son contrat d'API à un inconnu. La correction consiste à renvoyer une erreur générique côté client tout en journalisant le détail côté serveur, pour les équipes seulement.
# CORRIGÉ : message générique pour le client.
HTTP/1.1 400 Bad Request
{ "error": "Invalid request." } // Le détail reste utile — mais côté serveur uniquement.
function handleValidationError(err, req, res) {
logger.warn({ path: req.path, detail: err.message }); // interne
res.status(400).json({ error: 'Invalid request.' }); // public
} Mécanisme d'authentification
En observant l'en-tête Authorization, un attaquant identifie le schéma utilisé. Un en-tête Authorization: Basic am9lOjEyMzQ= trahit l'authentification HTTP basique (HTTP Basic Auth) : la chaîne n'est qu'un username:password encodé en base64, trivialement décodable (atob dans la console). Le base64 n'est pas du chiffrement.
| Schéma | Détail | Faiblesse à connaître |
|---|---|---|
| HTTP Basic Auth | username:password en base64 à chaque requête | Pas d'expiration ; interception facile ; exige TLS |
| HTTP Digest Auth | Hachage user:realm:password | Force liée à l'algorithme de hachage choisi |
| OAuth | Jeton « Bearer », connexion via un site tiers | Risque d'hameçonnage ; le site central compromis compromet tous les comptes liés |
Piège courant
Le base64 encode, il ne chiffre pas. L'authentification basique ne doit s'employer que sur un canal forcé en TLS/SSL, faute de quoi les identifiants circulent en clair — interceptables sur un Wi-Fi public. Préférez des jetons à durée de vie limitée et, idéalement, une authentification à deux facteurs (2FA) pour réduire la valeur d'un jeton volé.
Identifier les dépendances tierces — et ne pas les annoncer
Les applications modernes reposent massivement sur du code tiers : frameworks SPA, bibliothèques utilitaires, frameworks serveur, bases de données. Or une version connue d'une dépendance connue mène droit à une vulnérabilité connue, répertoriée dans une base CVE (Common Vulnerabilities and Exposures). L'attaquant n'a parfois même pas à concevoir d'exploit : il copie celui qui existe déjà.
Côté client : un livre ouvert
Le code client est téléchargé et lisible par tous. Les frameworks SPA exposent souvent une version dans un objet global (Ember.VERSION, React.version, Vue.version, l'attribut ng-version pour Angular). On énumère trivialement les scripts et feuilles de style tiers via le DOM :
// Ce qu'un visiteur voit déjà : tous vos scripts tiers.
document.querySelectorAll('script').forEach((s) => {
if (s.src) console.log(s.src);
});
// → jquery-3.4.1.min.js, stripe v3, analytics.js… On ne peut pas empêcher la lecture du code client — c'est intrinsèque au web. La défense est donc ailleurs : tenir ses dépendances à jour, surveiller les CVE des bibliothèques que l'on embarque, et supprimer les numéros de version inutiles des chemins de fichiers exposés.
Côté serveur : ne signez pas vos en-têtes
Le serveur, lui, peut se taire. Or par défaut, beaucoup de serveurs trop bavards révèlent leur identité dans les en-têtes HTTP.
# VULNÉRABLE : le serveur annonce son logiciel ET sa version.
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.5
X-Powered-By: ASP.NET
X-AspNet-Version: 4.0.25 Avec ces trois lignes, un attaquant connaît la pile exacte et peut chercher les CVE correspondantes. La correction est une simple configuration : retirer ou neutraliser ces en-têtes.
# CORRIGÉ : aucun en-tête ne trahit la pile.
HTTP/1.1 200 OK
Content-Type: application/json // Express : supprimer la signature par défaut.
app.disable('x-powered-by');
// ou : app.use(helmet()); qui s'en charge, entre autres protections. Pages d'erreur par défaut, signatures et bases de données
Même sans en-têtes bavards, un framework se trahit par sa page 404 par défaut ou ses messages d'erreur d'usine. Hoffman détaille comment, sur Ruby on Rails (open source), on peut identifier la version par empreinte (fingerprinting) en comparant le HTML de la page 404 aux modifications successives du dépôt Git — chaque changement daté donne une fourchette de versions, et donc une CVE applicable.
Les bases de données se devinent par leurs clés primaires. Le _id de MongoDB, par exemple, est un ObjectId de 12 octets au format hexadécimal, dont la structure (4 octets d'horodatage Unix, 5 aléatoires, 3 de compteur) est publiquement documentée. Repérer une telle clé dans une réponse JSON révèle la base sous-jacente.
Astuce
Trois réflexes défensifs ici. Remplacez systématiquement les pages d'erreur et 404 par défaut par des pages génériques personnalisées : elles ne doivent ni nommer le framework, ni laisser deviner sa version. Ne renvoyez jamais une erreur de base de données brute au client (elle révèle le moteur, et souvent une partie de la requête). Et n'exposez pas inutilement les clés primaires internes dans les réponses ; un identifiant opaque non devinable réduit ce que l'on apprend de votre stockage.
Repérer les points faibles d'architecture
Contre-intuitivement, Hoffman affirme que la plupart des vulnérabilités viennent d'une architecture mal pensée, pas de méthodes mal écrites. Deux applications comparables peuvent avoir l'une des dizaines de failles XSS, l'autre presque aucune — la différence est architecturale.
Sécurité par conception, et non au cas par cas
Considérez une messagerie privée. Le risque de XSS (cross-site scripting) existe à plusieurs couches : à l'écriture via l'API (POST), à l'écriture en base, à la lecture en base, à la lecture via l'API (GET), et à l'affichage côté client. Une application sécurisée n'éparpille pas des protections ponctuelles : elle abstrait la défense dans l'architecture. Plutôt que d'assainir le HTML à chaque endroit, on canalise tout affichage par une seule fonction sûre par défaut.
// VULNÉRABLE : chaque vue écrit directement dans le DOM.
element.innerHTML = message; // injection si message contient <script>
// CORRIGÉ : un point de passage unique, sûr par défaut.
import { DOMPurify } from '../utils/DOMPurify';
const appendToDOM = function(data, selector, unsafe = false) {
const element = document.querySelector(selector);
if (unsafe) {
element.innerHTML = DOMPurify.sanitize(data); // cas rare, explicite
} else {
element.innerText = data; // défaut sûr : pas d'interprétation HTML
}
}; Notez les choix de conception : le drapeau dangereux s'appelle unsafe, il vaut false par défaut, et il est le dernier paramètre — donc improbable à activer par accident. La présence de tels mécanismes est un signal d'architecture sécurisée ; leur absence, un signal de fragilité.
Plusieurs couches valent mieux qu'une
Le principe-clé : une application n'est aussi sûre que son maillon le plus faible. Si la défense XSS ne vit qu'à la couche « API POST », un nouvel endpoint d'envoi en masse moins rigoureux peut tout contourner et injecter des scripts en base. Des protections redondantes à plusieurs couches (API POST et écriture en base, par exemple) auraient stoppé l'attaque. Différentes couches permettent même des défenses différentes — un navigateur sans interface (headless browser) côté serveur peut détecter une exécution de script là où la base ne le pourrait pas.
À retenir
Cherchez, dans votre propre application, les fonctionnalités à faible ratio « mécanismes de sécurité / nombre de couches » : ce sont les plus susceptibles d'être exploitables, et donc à durcir en priorité. La défense en profondeur n'est pas un luxe : c'est l'aveu réaliste que chaque couche prise isolément peut être contournée.
Adoption contre réinvention
Dernier signal de risque : la tentation de réinventer des briques existantes. Réinventer une fonctionnalité purement fonctionnelle (un schéma de commentaires, un système de notifications) est acceptable. Réinventer ce qui exige une expertise pointue — cryptographie, bases de données, isolation de processus, gestion mémoire — est dangereux. La maxime de la profession : ne roulez jamais votre propre cryptographie. Un algorithme comme SHA-3 a été éprouvé pendant près de vingt ans par le NIST et les plus grandes firmes de sécurité ; l'adopter coûte zéro, le réinventer correctement coûterait des dizaines de millions. Les applications truffées de bases de données maison, de crypto maison et d'optimisations matérielles exotiques sont, en règle générale, les plus faciles à pénétrer.
Inventorier sa surface d'attaque
La conclusion défensive de toute la phase de recon est un inventaire. Concevez en supposant que l'attaquant connaît déjà votre pile, et tenez à jour des notes structurées — Hoffman recommande un format hiérarchique de type JSON — couvrant :
- les technologies employées (frameworks, serveurs, bases) ;
- la liste des endpoints d'API par verbe HTTP et leur forme ;
- les fonctionnalités de l'application (commentaires, authentification, notifications…) ;
- les domaines et sous-domaines utilisés ;
- les configurations de sécurité trouvées (par exemple une politique de sécurité de contenu, Content Security Policy, CSP) ;
- les systèmes d'authentification et de gestion de session.
Cet inventaire, fait de votre côté, est exactement la carte que l'attaquant cherche à reconstituer. Le construire vous-même vous donne une longueur d'avance : vous savez où se concentrent vos faiblesses, et vous pouvez les corriger avant qu'on ne les trouve.
À retenir
- La recon précède toute attaque : tout ce que votre application expose — sous-domaines, API, frameworks, versions — alimente la carte de l'attaquant. Votre meilleure défense est de dresser cet inventaire avant lui et de réduire ce qui est visible.
- Minimisez l'information divulguée : masquez les versions dans les en-têtes (
X-Powered-By,Server), remplacez les pages d'erreur et 404 par défaut par des pages génériques, et renvoyez des messages d'erreur vagues au client tout en journalisant le détail côté serveur. - Fermez les fuites structurelles : restreignez les transferts de zone DNS aux serveurs autorisés, n'exposez pas sur le réseau public les hôtes internes (
dev,staging,admin), et ne placez aucun secret dans le stockage côté client. - Centralisez l'autorisation : un contrôle d'accès unique et systématique vaut mieux que des vérifications recopiées endpoint par endpoint, où l'oubli est inévitable. Protégez profils, réinitialisations de mot de passe, messages privés et fonctions privilégiées.
- Architecturez la sécurité, ne la rapiécez pas : intégrez des défenses sûres par défaut (assainissement centralisé du DOM, drapeaux dangereux nommés et désactivés par défaut) et en plusieurs couches — une application ne vaut que son maillon le plus faible.
- Adoptez plutôt que réinventer ce qui exige une expertise rare, à commencer par la cryptographie : concevez en supposant que l'attaquant connaît déjà toute votre pile.