Sécurité des applications web : vulnérabilités, attaques courantes et exploitations

Sécurité des applications web

Les applications web sont aujourd’hui au coeur du fonctionnement des entreprises. Plateformes SaaS, espaces clients, applications métiers, outils internes, sites e-commerce, extranets ou interfaces d’administration concentrent des données sensibles et exposent des fonctionnalités critiques accessibles depuis un simple navigateur.

Cette exposition en fait naturellement une cible privilégiée. Pourtant, les compromissions les plus graves ne reposent pas toujours sur des techniques particulièrement sophistiquées. Elles résultent très souvent d’erreurs plus fondamentales : un contrôle d’accès oublié, une donnée fournie par le client considérée comme fiable, une fonctionnalité métier utilisable dans un contexte non prévu, un token insuffisamment lié à sa transaction ou encore deux composants d’une architecture qui n’interprètent pas une requête de la même manière.

La sécurité d’une application web ne peut donc pas être réduite à une liste de vulnérabilités techniques. Une application peut utiliser un framework récent, chiffrer ses communications et ne présenter aucune injection SQL évidente tout en permettant à un utilisateur de consulter les documents d’une autre organisation, de modifier son propre niveau de privilège, de rejouer une opération à usage unique ou de contourner une étape critique d’un processus métier.

Comprendre la sécurité applicative nécessite au contraire d’étudier l’application comme un ensemble de composants, d’identités, de données, de relations et de règles métier qui interagissent. Pour chaque grande famille de vulnérabilités, ce guide commence donc par le fonctionnement normal, identifie ensuite l’hypothèse de sécurité fragile, montre comment un pentester la remet en question et termine par les principes permettant de corriger durablement la cause du problème.

L’objectif est double. Le lecteur doit pouvoir comprendre pourquoi une vulnérabilité existe et comment elle peut être exploitée, mais également acquérir un modèle lui permettant d’analyser une application. Les exemples de requêtes et de code sont volontairement génériques et utilisent des domaines, comptes et identifiants fictifs.

Guide complet sur la sécurité des applications web

Comprendre la surface d’attaque d’une application web

Avant de rechercher une vulnérabilité, un pentester doit comprendre ce qu’il est réellement en train de tester. L’interface visible dans le navigateur ne représente qu’une partie de l’application. Elle repose généralement sur un ensemble plus vaste de composants : serveur applicatif, API, base de données, stockage objet, service d’authentification, moteur de génération de documents, services tiers, traitements asynchrones, cache, CDN, reverse proxy et parfois plusieurs services internes accessibles uniquement depuis le backend.

Chacun de ces composants reçoit des données, les interprète dans son propre contexte puis les transmet éventuellement à un autre composant. Les vulnérabilités apparaissent souvent lorsqu’une donnée franchit l’une de ces frontières sans que son niveau de confiance soit réévalué. Une chaîne enregistrée sans danger dans une base peut devenir une XSS lorsqu’elle est ensuite injectée dans du HTML. Une URL affichée comme du texte peut devenir une SSRF lorsque le serveur décide de la récupérer. Un identifiant d’organisation utilisé uniquement pour filtrer l’interface peut devenir un défaut d’isolation multi-tenant s’il est ensuite considéré comme une autorité par le backend.

Le premier travail consiste donc à cartographier ce que l’application expose réellement : fonctionnalités visibles et cachées, rôles, objets métier, états, flux de données, mécanismes d’authentification et composants intermédiaires. Cette cartographie ne produit pas encore une vulnérabilité, mais elle détermine la qualité de tout le raisonnement qui suivra.

Identifier les frontières de confiance

Une frontière de confiance apparaît chaque fois qu’une donnée passe d’un acteur ou d’un composant moins privilégié vers un contexte qui lui accorde davantage de pouvoir. Le passage du navigateur vers le serveur est la frontière la plus évidente, mais le passage du serveur vers une base SQL, d’un service public vers un réseau interne, d’une donnée utilisateur vers le navigateur d’un administrateur ou d’un reverse proxy vers le serveur applicatif en sont d’autres.

À chaque frontière, il faut se demander ce que le composant suivant suppose de la donnée. Est-elle simplement affichée ? Devient-elle une partie d’une requête ? Détermine-t-elle une ressource ? Déclenche-t-elle une connexion réseau ? Influence-t-elle une décision de sécurité ? Cette question de changement de contexte relie des vulnérabilités pourtant très différentes comme les injections, les SSRF, les XSS ou les défauts de contrôle d’accès.

Raisonner en invariants de sécurité

Une application robuste ne repose pas seulement sur une succession de contrôles ponctuels. Elle maintient des invariants, c’est-à-dire des propriétés qui doivent rester vraies quelle que soit la manière dont l’utilisateur interagit avec le système.

Dans une application SaaS, l’un de ces invariants peut être qu’un utilisateur d’une organisation ne peut jamais agir sur une ressource appartenant à une autre organisation. Dans un paiement, un autre invariant peut être qu’une transaction ne peut être confirmée que si le montant effectivement exécuté correspond au montant qui a été validé. Dans un parcours OAuth, l’application doit pouvoir établir que la réponse reçue appartient à la transaction qu’elle a initiée et qu’elle provient de l’émetteur attendu.

Le pentester cherche précisément les endroits où ces propriétés ne sont pas imposées par le serveur mais seulement supposées par les développeurs ou par le parcours normal de l’interface.

Contrôles d’accès et autorisations dans les applications web

Lors d’un test d’intrusion applicatif, les défauts de contrôle d’accès figurent régulièrement parmi les vulnérabilités ayant les conséquences les plus importantes. Leur danger vient notamment du fait qu’ils ne nécessitent pas toujours de technique complexe : un attaquant disposant d’un compte parfaitement légitime peut parfois accéder directement aux données d’un autre utilisateur, sortir de son tenant ou appeler une fonctionnalité réservée à un rôle plus privilégié.

Une confusion fondamentale doit être évitée. L’authentification répond à la question « qui êtes-vous ? ». L’autorisation répond à une autre question : « avez-vous le droit d’effectuer cette action sur cette ressource dans ce contexte précis ? ». Une application peut authentifier parfaitement ses utilisateurs tout en présentant des vulnérabilités d’autorisation critiques.

Comprendre les objets métier manipulés par l’application

Une application web peut être considérée comme un ensemble de fonctionnalités permettant de créer, consulter, modifier, partager, approuver ou supprimer des objets métier. Selon le contexte, ces objets sont des utilisateurs, factures, documents, commandes, projets, tickets support, paiements, contrats ou organisations.

Pour un utilisateur légitime, ces objets représentent simplement les données qu’il manipule. Pour un pentester, ils constituent les unités sur lesquelles les règles d’autorisation doivent être vérifiées.

GET /documents/1254
GET /documents/1255
GET /documents/1256

La présence d’un identifiant dans une URL n’est pas une vulnérabilité. Même un UUID imprévisible ne remplace jamais un contrôle d’accès. La question importante est la suivante : lorsque le serveur reçoit l’identifiant du document, vérifie-t-il que l’utilisateur authentifié est réellement autorisé à consulter ce document ?

Le même raisonnement s’applique aux identifiants placés dans les corps JSON, les paramètres de requête, les cookies, les messages WebSocket ou les appels d’API déclenchés en arrière-plan par l’interface.

Accéder aux données d’un autre utilisateur

Les vulnérabilités souvent décrites comme des IDOR correspondent en pratique à une famille plus large de défauts d’autorisation au niveau de l’objet. Une référence directe vers une ressource n’est pas dangereuse en elle-même ; elle le devient lorsque le serveur résout cette référence sans vérifier le contexte d’accès.

Contrôle de propriété d’une ressource

Prenons une plateforme de gestion comptable permettant à ses clients de télécharger leurs factures. La facture 4587 est exposée par un endpoint simple.

GET /invoice/4587

Le code serveur peut récupérer l’objet uniquement à partir de son identifiant :

public function getInvoice($invoiceId)
{
    $invoice = Invoice::find($invoiceId);

    if (!$invoice) {
        abort(404);
    }

    return response()->download($invoice->path);
}

Le développeur a correctement géré le cas d’une facture inexistante et l’endpoint peut même être protégé par une authentification. Pourtant, aucune condition ne relie la facture à l’utilisateur qui demande le téléchargement.

Pendant l’audit, le pentester peut créer une facture avec deux comptes de test et remplacer l’identifiant du premier par celui du second. Si le serveur retourne le fichier, le défaut de propriété est confirmé.

La remédiation consiste à intégrer la propriété dans la requête ou dans une politique d’autorisation centralisée :

$invoice = Invoice::where('id', $invoiceId)
    ->where('customer_id', auth()->id())
    ->firstOrFail();

Le serveur ne demande plus seulement si la facture existe. Il cherche une facture correspondant à l’identifiant demandé et appartenant au contexte de l’utilisateur authentifié. Cette différence traduit réellement la règle métier.

Contrôle du périmètre organisationnel dans les applications multi-tenant

Dans une application SaaS, la propriété individuelle est souvent insuffisante. Plusieurs utilisateurs appartiennent à la même organisation et partagent certaines ressources, mais les données de deux clients différents doivent rester strictement isolées.

GET /projects/782

Une récupération fondée uniquement sur l’identifiant du projet crée une dépendance implicite à la difficulté de deviner cet identifiant. Une requête plus robuste doit intégrer le tenant déterminé côté serveur :

return projectRepository
    .findByIdAndOrganization(
        id,
        auth.getOrganizationId()
    )
    .orElseThrow();

L’identifiant d’organisation ne devrait pas être choisi librement par le navigateur. Il doit provenir du contexte d’authentification ou d’une relation serveur maîtrisée. Dans un environnement multi-tenant, une erreur sur cette frontière peut exposer non pas une ressource isolée mais les données d’une entreprise entière.

Contrôle sur les relations métier

Toutes les autorisations ne peuvent pas être réduites à « cet objet appartient à cet utilisateur ». Un ticket support peut être accessible au demandeur, aux membres de son organisation et à certaines équipes internes. Un document peut être partagé avec plusieurs groupes. Une facture peut être consultée par la finance sans lui appartenir directement.

POST /tickets/1154/reply

Si le serveur récupère le ticket uniquement par son identifiant puis ajoute le message, le fait d’être authentifié ne garantit pas que l’utilisateur soit participant à la conversation. La politique correcte doit traduire la relation réelle : appartenance au tenant, participation au ticket, rôle support ou permission spécifique.

Cette logique explique pourquoi les modèles d’autorisation deviennent fragiles lorsqu’ils sont dispersés sous forme de conditions différentes dans chaque contrôleur. La règle métier doit rester cohérente partout où le même objet est manipulé.

Contrôle sur les actions sensibles

Un utilisateur peut être autorisé à lire une ressource sans pouvoir effectuer toutes les opérations disponibles sur celle-ci. Une plateforme documentaire peut permettre à de nombreux collaborateurs de consulter un document tout en réservant son approbation à un groupe plus restreint.

POST /documents/452/approve

Une vérification du rôle comme `DOCUMENT_APPROVER` améliore la situation mais ne suffit pas toujours. Un approbateur de l’organisation A ne doit probablement pas pouvoir approuver un document de l’organisation B. La décision doit donc intégrer simultanément l’utilisateur, sa permission, la ressource, le tenant et parfois l’état de cette ressource.

Accéder à des fonctionnalités plus privilégiées

Les défauts d’autorisation peuvent également conduire à une élévation verticale de privilèges. L’objectif n’est plus d’accéder aux données d’un utilisateur équivalent, mais d’appeler une fonction réservée à un administrateur, un manager ou un opérateur interne.

Contrôles appliqués uniquement dans l’interface

Masquer un bouton dans le frontend n’a aucune valeur de sécurité si l’endpoint correspondant reste accessible.

if (user.role === "admin") {
    displayDeleteButton();
}

Lorsque l’administrateur clique, le navigateur envoie par exemple :

POST /users/452/delete

Un utilisateur standard peut reproduire manuellement la même requête avec sa propre session. Si le backend ne contrôle pas le rôle ou la permission, la fonctionnalité est en réalité accessible indépendamment de ce que montre l’interface.

Fonctions d’administration exposées

Les routes d’administration peuvent être découvertes dans le JavaScript, la documentation, les conventions de nommage, des source maps ou des appels observés avec un compte privilégié.

GET /admin/users
GET /admin/logs
GET /admin/reports

L’absence de lien vers ces URLs ne constitue donc jamais une protection. Chaque endpoint doit appliquer sa propre politique d’autorisation côté serveur.

Gestion des utilisateurs et des rôles

Les fonctionnalités de gestion de compte figurent naturellement parmi les plus importantes à tester. Une requête comme celle-ci mérite immédiatement une vérification du demandeur :

POST /users/452/role
Content-Type: application/json

{
  "role": "admin"
}

Si le backend applique directement la valeur reçue sans contrôler la permission de l’appelant, un utilisateur peut parfois promouvoir son propre compte ou celui d’un complice. L’impact dépasse alors largement la simple fuite de données : l’attaquant acquiert les capacités normales d’un administrateur.

Exports et opérations à fort impact

Une autorisation doit également tenir compte de l’échelle de l’opération. Un commercial peut être autorisé à consulter un client sans être autorisé à récupérer toute la base via un export. Un utilisateur peut supprimer un objet individuel sans avoir le droit de lancer une suppression massive.

GET /customers/export

Les exports, actions groupées, changements de rôles, opérations comptables, publications et traitements automatiques méritent donc des permissions spécifiques, mais aussi une journalisation adaptée et parfois une réauthentification.

Concevoir un modèle d’autorisation robuste

Corriger un IDOR en ajoutant un `if` à l’endroit où il a été découvert est rarement suffisant. Une stratégie durable consiste à formaliser le modèle d’autorisation et à faire appliquer les mêmes règles partout.

Ne jamais faire confiance aux informations d’autorisation fournies par le client

{
  "document_id": 452,
  "owner_id": 128,
  "status": "approved"
}

Le navigateur peut modifier toutes ces valeurs. Même si elles ont été initialement générées par l’application, elles redeviennent non fiables lorsqu’elles reviennent depuis le client. Les décisions doivent reposer sur l’identité de la session, les permissions stockées côté serveur et les relations réelles en base.

Centraliser les politiques d’autorisation

Des contrôles différents dans chaque contrôleur finissent par produire des définitions contradictoires de la même règle. Une route vérifie le propriétaire, une autre l’organisation et une troisième uniquement le rôle administrateur.

Une politique centralisée permet au contraire de représenter explicitement une règle comme `canEditProject(user, project)`. Cette centralisation facilite le raisonnement, les revues de code, les tests automatisés et les évolutions futures.

Raisonner avec RBAC, ABAC et ReBAC

Le RBAC regroupe les permissions par rôles. Il reste pertinent lorsque les règles sont relativement stables et lisibles : un rôle `FINANCE` peut contenir `VIEW_INVOICE` et `EXPORT_INVOICES`, par exemple.

L’ABAC ajoute des attributs au raisonnement : département, niveau de sensibilité, environnement, propriété ou état. Le ReBAC s’intéresse davantage aux relations : appartient à l’organisation, participe au projet, gère le client, est propriétaire de l’espace.

Les applications SaaS complexes combinent souvent ces approches. Le point important est que la règle réelle du métier soit explicite plutôt que cachée dans une succession de conditions locales.

Appliquer deny by default et le moindre privilège

Le comportement par défaut doit être le refus. L’absence d’une règle d’autorisation ne devrait jamais signifier que l’action est automatiquement permise. Cette philosophie réduit le risque qu’un nouvel endpoint devienne accessible simplement parce qu’un contrôle a été oublié.

Le moindre privilège complète cette approche : chaque utilisateur, service ou compte technique ne doit disposer que des permissions réellement nécessaires à son fonctionnement.

Tester les autorisations comme une spécification métier

Les règles critiques devraient être couvertes par des tests automatisés. Il ne suffit pas de vérifier que l’utilisateur autorisé obtient un succès. Il faut également tester les utilisateurs d’un autre tenant, les profils moins privilégiés, les objets dans un état incompatible et les opérations alternatives comme download, delete, share ou export.

Une matrice d’autorisation devient alors une forme de documentation exécutable : elle décrit quels acteurs peuvent effectuer quelles actions sur quels objets.

Cas pratique de pentest : défaut d’isolation multi-tenant

Considérons une plateforme SaaS fictive utilisée par plusieurs entreprises pour gérer leurs contrats. Le pentester dispose de deux organisations de test, Alpha et Beta. Dans Alpha, il crée le contrat 1842. Dans Beta, il crée le contrat 2091.

Reconnaissance du comportement normal

Depuis l’organisation Alpha, l’ouverture du contrat produit la requête suivante :

GET /api/contracts/1842
Authorization: Bearer <token-alpha>

La réponse contient l’objet attendu. Le pentester observe que l’URL ne contient aucun identifiant d’organisation et que le token Alpha semble être le seul élément fournissant le contexte du tenant.

Test de séparation entre tenants

Il remplace uniquement l’identifiant du contrat par celui créé dans Beta tout en conservant le token Alpha.

GET /api/contracts/2091
Authorization: Bearer <token-alpha>

Le serveur retourne le contrat de Beta. Le test démontre que la recherche est probablement fondée sur l’identifiant global du contrat sans filtrage par organisation.

Le pentester ne s’arrête pas à la lecture. Il vérifie ensuite si la même faiblesse existe sur la modification, le téléchargement des pièces jointes et l’archivage. Cette étape permet de déterminer si le problème concerne un endpoint isolé ou le modèle d’autorisation complet.

Qualification de l’impact

L’impact ne dépend pas uniquement du fait qu’un contrat puisse être consulté. Il faut déterminer si les identifiants sont découvrables, si l’exploitation peut être automatisée et quelles données sont contenues dans les contrats. Dans une plateforme multi-tenant, la possibilité d’accéder à des documents de clients différents remet directement en cause la promesse d’isolation du service.

Remédiation attendue

La correction durable consiste à imposer le tenant dans toutes les résolutions d’objets et à centraliser la politique. Un correctif limité à `GET /api/contracts/{id}` serait incomplet si PUT, DELETE ou les pièces jointes restaient fondés sur le seul identifiant.

Authentification web : sécuriser l’ensemble du parcours utilisateur

L’authentification ne se limite pas au formulaire de connexion. Inscription, invitation, validation d’e-mail, changement de mot de passe, récupération du compte, MFA, magic links, passkeys et SSO sont autant de chemins permettant de prouver ou de retrouver une identité.

Un attaquant ne cherche pas nécessairement à casser le mécanisme principal. Il cherche le chemin le plus faible conduisant au même niveau d’accès. Une application peut imposer un MFA très robuste lors du login tout en proposant une récupération de compte qui permet de désactiver ce facteur avec un contrôle beaucoup moins fort.

Les failles dans les parcours d’inscription et d’invitation

Dans les applications B2B, une invitation agit souvent comme une preuve temporaire permettant de rejoindre une organisation. La sécurité du token doit donc être comparable à celle d’un secret d’authentification temporaire.

Invitations réutilisables ou trop longues

Un lien d’invitation doit être imprévisible, limité dans le temps et invalidé après son utilisation ou sa révocation. Un ancien e-mail ne devrait pas permettre de rejoindre l’organisation plusieurs mois plus tard si l’invitation n’est plus valable.

La logique d’association doit également être maintenue côté serveur. Une requête qui contient `organization_id` ou `role` ne doit pas permettre à l’utilisateur de choisir librement ces valeurs si elles ont déjà été définies lors de la création de l’invitation.

Comptes créés avec des privilèges excessifs

POST /api/signup
Content-Type: application/json

{
  "email": "alice@example.test",
  "role": "admin"
}

Une API ne devrait pas accepter un champ de rôle simplement parce que l’objet interne possède cette propriété. Le serveur doit déterminer lui-même les privilèges initiaux à partir du parcours métier et de l’invitation éventuelle.

Validation d’adresse e-mail contournable

Une page demandant à l’utilisateur de valider son e-mail n’est pas un mécanisme de sécurité si les endpoints accessibles derrière cette page ne contrôlent jamais le statut réel du compte. Le serveur doit appliquer la règle sur les actions concernées et non uniquement sur la navigation du frontend.

Énumération d’utilisateurs

Les parcours d’authentification peuvent révéler l’existence d’un compte par leurs messages, codes HTTP, temps de réponse ou comportements secondaires. Une page affichant « adresse inconnue » pour un utilisateur absent et « mot de passe incorrect » pour un compte existant produit un oracle immédiat.

Des messages identiques ne suffisent pas toujours. Le pentester peut comparer la taille de réponse, la latence, les e-mails envoyés ou les réactions d’autres endpoints comme l’inscription et le reset. L’objectif défensif est d’éviter de fournir un mécanisme fiable et automatisable de découverte des comptes lorsque cette information présente un risque.

Brute force, password spraying et credential stuffing

Le brute force teste de nombreux mots de passe contre un compte. Le password spraying préfère quelques mots de passe fréquents appliqués à un grand nombre de comptes, ce qui contourne plus facilement les seuils basés uniquement sur un utilisateur. Le credential stuffing exploite des identifiants déjà compromis ailleurs et réutilisés par les utilisateurs.

Une défense efficace considère donc plusieurs signaux : fréquence, compte, source réseau, historique, contexte de connexion et présence d’un second facteur. Un verrouillage absolu après quelques erreurs peut lui-même devenir un outil de déni de service contre les utilisateurs.

Stocker correctement les mots de passe

Un mot de passe ne doit ni être stocké en clair ni chiffré de manière réversible uniquement afin de vérifier sa valeur lors de la connexion. Il doit être traité par une fonction spécifiquement conçue pour le stockage des mots de passe, avec un coût adapté aux capacités matérielles actuelles.

L’intérêt d’une fonction comme Argon2id vient notamment de son coût calculatoire et mémoire. Après une fuite de base, l’attaquant peut tester les candidats hors ligne sur son propre matériel ; le rate limiting de l’application et le MFA ne ralentissent plus ce calcul.

Chaque mot de passe doit disposer d’un sel unique. Un pepper stocké séparément peut fournir une défense supplémentaire dans certaines architectures, mais il ne remplace ni le sel ni un algorithme approprié.

Les pièges de la réinitialisation de mot de passe

La fonctionnalité « mot de passe oublié » est un second mécanisme d’authentification. La possession du token de reset équivaut temporairement à une preuve permettant de prendre le contrôle du compte.

Tokens prévisibles, réutilisables ou mal invalidés

Le token doit être généré par une source cryptographiquement sûre, lié au bon compte, limité dans le temps et invalidé immédiatement après son utilisation. Une valeur construite à partir d’un identifiant et d’un timestamp peut sembler longue tout en restant prévisible si son entropie réelle est faible.

Après une réinitialisation réussie, l’ancien lien ne doit pas rester utilisable. Un changement de mot de passe peut également justifier la révocation d’autres sessions selon le modèle de menace.

Password reset poisoning

Certaines applications construisent dynamiquement l’URL de reset à partir du Host reçu dans la requête. Cette pratique devient dangereuse si l’infrastructure accepte une valeur contrôlée par le client.

reset_url = "https://" + request.host + "/reset/" + token

Un lien de sécurité ne devrait pas dépendre d’un nom d’hôte fourni arbitrairement par l’utilisateur. L’application doit utiliser une origine canonique issue de sa configuration ou d’une liste stricte de valeurs de confiance.

Contournements de l’authentification multifacteur

Le MFA doit être représenté comme un état d’authentification côté serveur et non comme une simple page intermédiaire. Une session ayant validé uniquement le mot de passe ne doit pas être considérée comme équivalente à une session ayant terminé le second facteur.

Le pentester teste donc les endpoints accessibles avant la validation, les parcours alternatifs, la récupération du compte, les anciennes API et les fonctions permettant de modifier ou désactiver le facteur. Un second facteur robuste ne protège pas un endpoint oublié qui considère déjà la session comme pleinement authentifiée.

Un magic link remplace essentiellement le mot de passe par un token transmis par e-mail. Les propriétés attendues sont donc similaires à celles d’un reset : imprévisibilité, expiration courte, usage unique et liaison au compte correct.

Les passkeys reposent sur des mécanismes cryptographiques liés au site et offrent notamment une forte résistance au phishing traditionnel. Elles ne dispensent toutefois pas de sécuriser la récupération du compte. Si un compte protégé par passkey peut être récupéré avec une preuve nettement plus faible, cette procédure devient la frontière réelle de sécurité.

Réauthentifier avant les opérations les plus sensibles

Une session valide peut avoir été créée plusieurs heures auparavant ou être utilisée sur un poste momentanément abandonné. Certaines actions justifient donc une nouvelle preuve de présence de l’utilisateur : changement de mot de passe, désactivation du MFA, modification de l’adresse principale, création d’une clé API ou ajout d’un administrateur d’organisation.

Cette réauthentification ne remplace pas le contrôle d’autorisation. Elle ajoute une assurance supplémentaire pour des actions dont l’impact justifie une élévation temporaire du niveau de confiance.

OAuth 2.0, OpenID Connect et sécurité du SSO

OAuth 2.0 et OpenID Connect sont omniprésents dans les applications modernes mais restent régulièrement mal compris. OAuth est avant tout un framework de délégation d’autorisation ; OpenID Connect ajoute une couche d’identité utilisée notamment pour les connexions fédérées.

Le danger vient rarement du protocole abstrait pris isolément. Il apparaît lorsque le client, l’Authorization Server et les APIs n’appliquent pas les mêmes hypothèses sur les redirect URIs, les tokens, l’émetteur ou la transaction en cours.

Comprendre les acteurs et le rôle de chaque composant

Dans un déploiement classique, le navigateur de l’utilisateur interagit avec un client, souvent l’application web. Le client délègue l’authentification ou l’autorisation à un Authorization Server. Les access tokens obtenus servent ensuite à accéder à un Resource Server, c’est-à-dire une API protégée.

OpenID Connect ajoute notamment l’ID Token, qui permet au client de recevoir des informations sur l’authentification de l’utilisateur. Cette architecture multiplie les frontières de confiance : le client doit savoir de quel émetteur provient le token, pour quelle audience il a été émis et à quelle transaction il appartient.

Déroulement d’un Authorization Code Flow

Lorsqu’un utilisateur choisit de se connecter, le navigateur est redirigé vers l’Authorization Server avec un ensemble de paramètres décrivant la transaction.

GET /authorize?
  client_id=client123
  &redirect_uri=https://app.example.test/callback
  &response_type=code
  &state=<valeur-aleatoire>
  &code_challenge=<valeur>
  &code_challenge_method=S256

Après l’authentification, le navigateur revient vers le client avec un authorization code. Le backend échange ensuite ce code contre les tokens attendus.

GET /callback?code=<authorization-code>&state=<valeur-aleatoire>

Chaque paramètre important répond à une question de sécurité. La `redirect_uri` détermine où le navigateur peut revenir. `state` aide à lier la réponse à une transaction initiée par le client. PKCE lie l’échange du code à un secret éphémère connu du client. Dans OpenID Connect, `nonce` peut lier l’ID Token à la transaction.

Validation stricte de redirect_uri

Un Authorization Server ne doit pas accepter une redirection arbitraire ni appliquer des règles trop permissives. Une comparaison par préfixe peut par exemple accepter une URL qui appartient techniquement à une autre destination ou à un endpoint d’open redirect.

Les bonnes pratiques modernes imposent une validation stricte des redirect URIs enregistrées. L’objectif est d’empêcher qu’un authorization code ou token soit envoyé vers un endpoint contrôlé par l’attaquant.

Rôle du paramètre state

`state` permet au client de relier la réponse reçue à la transaction qu’il avait créée. Une valeur constante ou non vérifiée ne fournit pas cette propriété.

Le client doit générer une valeur imprévisible, l’associer à la session ou à la transaction et vérifier exactement sa présence au retour. Selon l’architecture, d’autres mécanismes peuvent également fournir des garanties anti-CSRF, mais `state` reste une notion essentielle à comprendre dans les flows de redirection.

PKCE et protection de l’authorization code

PKCE utilise un `code_verifier` généré par le client et un `code_challenge` dérivé de ce verifier. Seul le challenge est envoyé au début de la transaction. Lors de l’échange final, le client présente le verifier original.

Un authorization code intercepté devient donc insuffisant s’il n’est pas accompagné du verifier associé. Les recommandations actuelles étendent largement l’usage de PKCE au-delà de son contexte historique des applications natives, y compris pour les applications web lorsque cela est applicable.

OpenID Connect et nonce

`nonce` permet de lier un ID Token à une transaction d’authentification. Une valeur transactionnelle correctement vérifiée réduit certains scénarios de rejeu ou de confusion de réponses.

Comme `state` et le challenge PKCE, un nonce constant perd l’essentiel de sa valeur. Ces paramètres ne sont pas de simples champs obligatoires destinés à faire fonctionner une bibliothèque ; ils matérialisent une propriété de sécurité précise.

Valider réellement un ID Token

Décoder un JWT ne signifie pas le valider. Une application ne devrait jamais extraire simplement un e-mail du payload puis lui faire confiance.

{
  "iss": "https://idp.example.test",
  "aud": "client123",
  "sub": "248289761001",
  "email": "alice@example.test",
  "exp": 1770000000
}

La signature doit être vérifiée avec les clés attendues, mais ce n’est qu’une partie du contrôle. L’émetteur doit correspondre à l’Identity Provider configuré, l’audience doit inclure le client prévu, la durée de validité doit être respectée et les autres claims nécessaires au flow doivent être cohérents.

Un token peut donc être cryptographiquement valide tout en étant inacceptable pour cette application. La bonne question n’est pas « ce JWT est-il signé ? » mais « ce token est-il valide pour ma transaction, mon client et mon émetteur ? ».

Account linking et confusion d’identité

La liaison entre une identité externe et un compte local constitue une zone particulièrement sensible. Supposons qu’un compte local `alice@example.test` existe déjà et qu’un nouvel Identity Provider affirme la même adresse.

L’application ne devrait pas automatiquement conclure que les deux identités représentent la même personne sans avoir défini les règles de confiance associées à ce fournisseur et à la vérification de l’adresse. Le pentester étudie donc la création automatique de comptes, les changements d’e-mail, l’ajout ou le retrait de fournisseurs et les flows permettant de lier une identité supplémentaire.

Refresh tokens et persistance de session

Un access token est souvent volontairement court. Le refresh token permet d’en obtenir de nouveaux sans authentifier de nouveau l’utilisateur, ce qui lui confère une valeur particulièrement importante.

La rotation des refresh tokens permet d’émettre un nouveau token à chaque utilisation et de détecter certains scénarios de réutilisation d’un token plus ancien. Le stockage et la transmission de ces tokens doivent être conçus en fonction du type de client et du modèle de menace.

Applications web modernes et clients exécutés dans le navigateur

Les applications riches exécutées dans le navigateur doivent traiter avec prudence les secrets et tokens accessibles au JavaScript. Les recommandations récentes pour les browser-based applications insistent sur la réduction de l’exposition des tokens, l’utilisation de flows modernes avec PKCE et, lorsque l’architecture le permet, sur des modèles où un backend dédié peut conserver certains secrets hors du contexte JavaScript.

Le choix entre SPA pure, backend-for-frontend ou autre architecture ne doit pas être dogmatique. Il doit découler des capacités nécessaires, de la surface XSS et du niveau de protection attendu pour les tokens.

Cas pratique de pentest : parcours OpenID Connect mal lié

Considérons une application fictive proposant un SSO d’entreprise. Le pentester observe une redirection vers l’Identity Provider avec `state` et PKCE. Il commence par documenter le flow normal, les cookies créés et les paramètres persistés dans la session.

Tester la liaison de la transaction

Le premier test consiste à initier deux connexions dans deux sessions séparées, puis à vérifier si une réponse appartenant à la première transaction peut être traitée dans le contexte de la seconde. L’objectif n’est pas de forcer un token invalide mais de déterminer si le client relie correctement code, state, session et émetteur.

Si l’application accepte un `state` absent ou constant, l’observation doit être approfondie. Le pentester vérifie notamment si PKCE ou d’autres mécanismes offrent déjà une liaison transactionnelle et si le comportement ouvre réellement un scénario exploitable.

Tester les redirect URIs et les open redirects

Le pentester compare la `redirect_uri` envoyée avec les valeurs enregistrées. Il vérifie si des variations de chemin, sous-domaine ou encodage sont acceptées et si une URL autorisée contient elle-même une fonction de redirection ouverte. Le but est de confirmer la propriété de sécurité, pas uniquement de constater qu’un paramètre est modifiable.

Tester la validation des tokens

Dans un environnement de test contrôlé, le pentester vérifie que les tokens destinés à un autre client, émis par un autre environnement ou arrivés à expiration sont refusés. Il observe également comment l’application relie le compte local au `sub` externe et si l’adresse e-mail est utilisée comme unique identifiant de confiance.

Remédiation et contre-test

La correction attendue repose généralement sur les bibliothèques conformes du protocole, une validation stricte des redirect URIs, des valeurs transactionnelles vérifiées, PKCE, une validation complète de l’émetteur et de l’audience et une politique explicite d’account linking. Le contre-test doit rejouer les cas de confusion identifiés et confirmer que l’application refuse les réponses hors contexte.

Gestion de session et sécurité des cookies

Une fois l’utilisateur authentifié, l’application doit reconnaître ses requêtes suivantes. Dans une architecture classique, cette continuité repose sur un identifiant de session transmis dans un cookie. Cette valeur devient donc un secret d’authentification : sa fuite permet potentiellement de reprendre la session sans connaître le mot de passe.

Comment fonctionne réellement une session web

Le serveur associe un identifiant aléatoire à un état comprenant l’utilisateur, ses propriétés de session et parfois son niveau d’authentification. Le navigateur stocke l’identifiant puis le renvoie automatiquement lorsque le domaine, le chemin et les attributs du cookie correspondent à la requête.

La sécurité ne dépend donc pas seulement de la force de l’identifiant. Elle dépend aussi de la manière dont il est transmis, renouvelé, invalidé et associé aux changements de privilège.

Protéger les cookies de session

L’attribut `Secure` demande au navigateur de transmettre le cookie uniquement via HTTPS. `HttpOnly` empêche le JavaScript de lire directement sa valeur. `SameSite` contrôle plusieurs contextes cross-site dans lesquels le cookie peut être joint à une requête.

Ces attributs répondent à des menaces différentes. `HttpOnly` réduit le vol direct d’un cookie par une XSS mais ne supprime pas la capacité du script injecté à agir depuis l’origine légitime. `SameSite` réduit certains scénarios CSRF mais ne remplace pas nécessairement tous les mécanismes de validation d’intention.

Session fixation et renouvellement de l’identifiant

Une session fixation apparaît lorsqu’un attaquant connaît ou influence l’identifiant utilisé par la victime avant son authentification. Si le serveur conserve ce même identifiant après la connexion, la session désormais authentifiée peut rester associée à une valeur déjà connue.

Le serveur doit donc renouveler l’identifiant lors des changements importants de niveau de confiance, notamment après l’authentification, une réauthentification forte ou certaines élévations de privilèges.

Vol, réutilisation et invalidation de session

Une session peut fuiter via une XSS, des logs, un stockage navigateur inadapté, une extension malveillante, un poste compromis ou une erreur d’architecture. Une déconnexion efficace doit invalider l’état serveur lorsque le modèle de session le permet, et certains événements comme un changement de mot de passe, une compromission déclarée ou une modification importante des privilèges peuvent justifier la révocation des autres sessions.

Le pentester vérifie donc ce qui se passe après un logout, après une modification du mot de passe et après la désactivation d’un compte. Une session ancienne ne doit pas continuer à bénéficier de privilèges qui ne sont plus valides.

JWT : signatures, durée de vie et révocation

Les JSON Web Tokens sont fréquemment utilisés dans les architectures modernes. Un JWT signé protège principalement l’intégrité de son contenu ; il ne rend pas automatiquement celui-ci confidentiel. Les claims ne doivent donc pas contenir de secrets simplement parce que la chaîne obtenue semble opaque.

La signature ne suffit pas

Une validation complète vérifie l’algorithme attendu, la signature, l’émetteur, l’audience, l’expiration et les autres claims requis par le protocole ou l’application. Un token signé par une clé légitime peut être valide pour un autre service et doit alors être rejeté.

Le pentester cherche notamment les services qui partagent les mêmes clés ou le même Identity Provider mais qui n’appliquent pas les mêmes contraintes d’audience. Le simple fait qu’un token soit accepté par une bibliothèque JWT ne signifie pas qu’il appartient au bon contexte applicatif.

Durée de vie excessive des access tokens

Un access token valide plusieurs semaines transforme chaque fuite en compromission potentiellement durable. Des tokens plus courts, associés à un mécanisme de renouvellement correctement protégé, réduisent la fenêtre d’exploitation d’une fuite isolée.

Cette durée doit rester cohérente avec l’impact d’un vol, la fréquence des interactions et la capacité de l’application à révoquer ou détecter des sessions anormales.

Révocation et refresh tokens

L’idée selon laquelle un JWT serait impossible à révoquer est trop simplificatrice. Une architecture peut conserver un état minimal, des listes de révocation pour certains événements critiques ou utiliser des access tokens courts et des refresh tokens contrôlés.

Les refresh tokens méritent une protection supérieure car ils permettent de renouveler la capacité d’accès. Leur rotation et la détection de réutilisation réduisent certains scénarios de vol persistant.

Cross-Site Scripting : comprendre et prévenir les XSS

Une XSS apparaît lorsqu’une donnée qui aurait dû rester une donnée est interprétée par le navigateur comme du contenu actif. Cette définition est plus importante que la mémorisation de quelques payloads : le problème fondamental est une confusion entre données et code dans un contexte navigateur.

Comprendre sources, transformations et sinks sans simplifier à l’excès

Prenons un commentaire enregistré dans une base. Tant qu’il reste une chaîne de caractères, il n’exécute rien. Le risque apparaît plus tard lorsque le frontend décide comment cette chaîne doit être insérée dans le document.

const comment = response.comment;
commentContainer.innerHTML = comment;

Avec `innerHTML`, le navigateur interprète la chaîne comme du HTML. Une variante utilisant `textContent` demande au contraire explicitement de traiter la valeur comme du texte.

commentContainer.textContent = response.comment;

Le même contenu peut donc être sûr dans un stockage ou un affichage texte puis dangereux dans un contexte HTML. Il faut raisonner sur le trajet complet de la donnée et sur sa destination finale, pas uniquement sur l’endroit où elle a été saisie.

XSS reflétée

Une XSS réfléchie apparaît lorsqu’une valeur provenant de la requête est immédiatement réinjectée dans la réponse. Une page de recherche peut par exemple afficher le terme saisi sans l’encoder correctement pour le contexte HTML.

Le pentester cherche alors à déterminer si des caractères actifs sont interprétés par le navigateur et si la valeur traverse d’autres contextes, par exemple un attribut ou une chaîne JavaScript. Le payload utilisé dépend du contexte exact ; une preuve de concept minimaliste suffit à confirmer l’exécution.

L’impact dépend ensuite du parcours nécessaire pour atteindre la victime. Une XSS réfléchie peut nécessiter l’ouverture d’une URL préparée, mais elle s’exécute malgré tout dans l’origine de l’application et bénéficie des capacités accordées à ce contexte.

XSS stockée

Dans une XSS stockée, la donnée est enregistrée puis affichée plus tard. Cette variante peut être plus dangereuse parce que l’attaquant n’a pas besoin de convaincre chaque victime de visiter une URL spécialement construite. Un commentaire, un champ de profil, un ticket support ou un nom de fichier peuvent déclencher l’exécution lorsqu’un autre utilisateur consulte la page.

Le scénario devient particulièrement important lorsque la donnée est affichée dans un back-office utilisé par des profils plus privilégiés. Le pentester cherche donc les différences de rendu entre l’interface d’un utilisateur standard et celle des équipes support ou administrateurs.

DOM XSS

Une DOM XSS peut exister sans que le serveur ne réinjecte lui-même la donnée dans le HTML. Le JavaScript lit une valeur depuis `location`, `postMessage`, `localStorage` ou une réponse API puis la transmet à une primitive dangereuse.

const message = location.hash.substring(1);
document.getElementById("output").innerHTML = message;

L’analyse nécessite donc de suivre le flux dans le code client, y compris les transformations intermédiaires. Un sanitizer appliqué trop tôt peut être rendu inefficace si une transformation ultérieure recompose du HTML dangereux.

Le contexte d’injection détermine la défense

Une donnée utilisée dans le texte HTML, un attribut, une URL ou du JavaScript ne nécessite pas exactement le même traitement. C’est pourquoi les mécanismes d’encodage doivent être contextuels et pourquoi les APIs sûres du framework sont préférables à la construction manuelle de chaînes.

Les frameworks modernes échappent souvent les valeurs par défaut. Les vulnérabilités réapparaissent fréquemment lorsque le développeur désactive explicitement cette protection pour permettre l’injection de contenu riche ou qu’une bibliothèque tierce manipule le DOM d’une manière moins sûre.

Markdown, WYSIWYG et contenus riches

Certaines applications doivent réellement accepter une partie du HTML. Un encodage intégral détruirait alors la fonctionnalité attendue. Il faut utiliser une bibliothèque de sanitization capable de conserver les balises autorisées tout en supprimant les attributs, protocoles ou constructions dangereuses.

La sécurité doit être évaluée après toutes les transformations. Un contenu peut être nettoyé, converti en Markdown, enrichi par un plugin puis sérialisé de nouveau. Le résultat final transmis au navigateur est le seul état qui permette de conclure.

Le choix des éléments autorisés doit également être lié au besoin métier. Autoriser des balises ou attributs complexes « au cas où » augmente inutilement la surface de traitement.

Ce qu’une XSS permet réellement

La démonstration classique avec une boîte de dialogue prouve uniquement que du JavaScript contrôlé s’exécute. L’impact réel dépend du contexte de la victime. Dans une session authentifiée, le code peut souvent appeler les mêmes APIs que l’utilisateur, lire les données accessibles au JavaScript et modifier l’interface.

Un cookie `HttpOnly` peut empêcher la lecture directe de l’identifiant de session, mais il n’empêche pas le navigateur d’attacher ce cookie aux requêtes émises depuis la page légitime. Une XSS exécutée chez un administrateur peut donc déclencher des actions administratives même sans extraire le cookie.

Il faut également distinguer les tokens placés dans un stockage accessible à JavaScript des cookies protégés par `HttpOnly`. La conséquence d’une XSS dépend fortement de cette architecture et le rapport doit éviter les affirmations génériques qui ne correspondent pas au comportement réellement observé.

CSP, nonces et Trusted Types

Une Content Security Policy stricte peut limiter certaines conséquences en contrôlant les sources de scripts et les modes d’exécution. Les politiques modernes basées sur des nonces ou des hashes sont généralement plus robustes que celles qui autorisent largement `unsafe-inline`.

CSP reste toutefois une défense en profondeur. Elle ne remplace pas l’encodage, la sanitization ni l’utilisation d’APIs DOM sûres. Trusted Types peut compléter cette stratégie dans les applications JavaScript complexes en limitant certains sinks DOM sensibles.

Cas pratique de pentest : XSS stockée visant un back-office

Une plateforme de support permet à un client de créer un ticket. Le frontend client affiche correctement la description comme texte. Quelques minutes plus tard, le pentester observe qu’un agent support dispose d’un back-office distinct qui rend la même description sous forme de contenu riche.

Identifier la divergence de rendu

Le pentester commence par envoyer un contenu HTML inoffensif, par exemple une mise en forme simple, afin de déterminer si le back-office l’interprète. Cette étape évite de conclure trop vite à partir du seul frontend public.

Lorsque le HTML est effectivement interprété, il utilise ensuite une preuve de concept minimale permettant de confirmer l’exécution dans le navigateur de test de l’agent. La démonstration reste volontairement limitée et n’effectue aucune action destructive.

Évaluer le contexte de la victime

Le point critique n’est pas le script en lui-même mais les capacités du compte support. Le pentester examine les endpoints disponibles, les données visibles et les opérations qui pourraient être appelées depuis l’origine. Si le support peut réinitialiser un compte, générer une invitation ou consulter des données sensibles, l’impact dépasse largement la simple modification visuelle de la page.

Vérifier les protections partielles

Si le cookie de session est `HttpOnly`, le rapport ne doit pas prétendre qu’il peut nécessairement être volé. Il faut décrire l’impact réellement démontré : exécution de code dans l’origine, accès aux données disponibles au JavaScript et capacité à déclencher des requêtes authentifiées.

Une CSP présente doit également être analysée. Si la preuve de concept fonctionne malgré cette politique, le rapport précise la configuration qui a permis l’exécution ; si la CSP bloque certains chemins, elle est reconnue comme une défense en profondeur sans masquer la vulnérabilité de rendu.

Corriger la cause racine

La correction doit intervenir au niveau du rendu du back-office : encodage contextuel pour le texte ou sanitization robuste si un sous-ensemble HTML est réellement nécessaire. Une CSP stricte peut ensuite réduire la surface résiduelle mais ne remplace pas cette correction.

Cross-Site Request Forgery (CSRF)

Le CSRF exploite le comportement normal du navigateur lorsqu’il associe automatiquement certains éléments d’authentification, notamment les cookies, à une requête. Un site malveillant tente alors de provoquer une action vers une application dans laquelle la victime possède déjà une session valide.

Le serveur voit une requête authentifiée mais doit encore déterminer si elle correspond à une intention légitime de l’utilisateur.

Actions sensibles et état serveur

Les changements d’e-mail, de mot de passe, de permissions, de moyens de paiement, de clés API et les opérations administratives sont des cibles classiques. Les requêtes GET ne devraient pas provoquer de modifications importantes, car elles sont plus facilement déclenchées par des mécanismes de navigation ou de chargement de ressources.

Le pentester recense donc les endpoints à effet de bord puis analyse la manière dont le navigateur leur transmet l’authentification et les preuves d’intention.

Tokens CSRF et validation d’origine

Une défense classique ajoute un secret lié à la session et exige sa présence sur les requêtes sensibles. Le serveur peut également valider l’`Origin` ou le `Referer` dans les contextes appropriés. Les APIs qui nécessitent des en-têtes non simples bénéficient en outre des protections de la Same Origin Policy et de CORS, à condition que CORS soit lui-même correctement configuré.

Ajouter un champ caché dans le formulaire n’a aucune valeur si le serveur n’en vérifie pas réellement la valeur. De même, un token qui n’est pas lié à la session ou qui reste constant pour tous les utilisateurs n’apporte pas la propriété attendue.

Pourquoi SameSite ne doit pas être surinterprété

`SameSite` réduit fortement plusieurs scénarios CSRF modernes, mais la notion de site n’est pas strictement identique à celle d’origine et certaines architectures de sous-domaines conservent des relations de confiance particulières.

Pour les actions les plus sensibles, il est donc préférable de considérer `SameSite` comme une couche de défense plutôt que comme l’unique mécanisme d’intention. Le choix de `Lax`, `Strict` ou `None` doit également correspondre aux besoins réels du parcours utilisateur.

Injection SQL dans les applications web

Une injection SQL apparaît lorsqu’une donnée utilisateur influence la structure d’une requête au lieu d’être traitée uniquement comme une valeur. La vulnérabilité ne provient pas du fait d’utiliser SQL ; elle naît de la perte de séparation entre le langage et la donnée.

Comment une requête devient vulnérable

Prenons une fonctionnalité de recherche dont le code construit directement la requête :

query = "SELECT id, name FROM products WHERE name = '" + search + "'"

Avec une valeur ordinaire, la requête semble fonctionner correctement. Mais le moteur de base reçoit une seule chaîne et ne sait pas quelle portion devait être considérée comme une donnée. Si la valeur contient des éléments de syntaxe SQL, ils peuvent être interprétés comme une partie de l’instruction.

Cette propriété est la cause racine de la vulnérabilité. Elle explique également pourquoi les filtres de caractères sont rarement une bonne correction : ils essaient de reconnaître les manifestations d’une injection au lieu d’empêcher la donnée de devenir du code.

Pourquoi les requêtes paramétrées changent le modèle de sécurité

cursor.execute(
    "SELECT id, name FROM products WHERE name = %s",
    (search,)
)

La structure SQL et la valeur sont ici transmises séparément. Le moteur traite `search` comme une donnée et non comme un fragment de langage. Cette propriété est fondamentalement plus robuste qu’une blacklist de caractères qui tenterait d’anticiper toutes les formes possibles d’une attaque.

Le paramétrage doit être réalisé par l’API du driver ou de l’ORM. Ajouter soi-même des guillemets autour d’une valeur échappée n’offre pas nécessairement les mêmes garanties et réintroduit facilement des erreurs de contexte.

Identifier une injection pendant un audit

Le pentester commence par les fonctionnalités susceptibles de construire des requêtes dynamiques : recherche, filtres, reporting, exports, tris ou interfaces d’administration. Il modifie progressivement les valeurs pour observer des erreurs, différences de résultat ou variations de temps.

Une erreur SQL explicite est un indice fort mais n’est pas nécessaire. Une application peut masquer correctement les erreurs tout en restant vulnérable. L’objectif est d’établir qu’une donnée influence la logique de la requête d’une manière non prévue.

Le test doit rester proportionné. Une preuve minimale est généralement suffisante pour établir la vulnérabilité sans effectuer une extraction massive de données réelles.

Recherche, filtres et requêtes dynamiques

Les filtres complexes créent souvent davantage de surface que le formulaire de login. Un moteur de recherche peut construire dynamiquement plusieurs clauses, combiner des opérateurs ou traduire un JSON en requête.

Chaque portion dynamique doit être distinguée. Les valeurs peuvent être paramétrées, mais une colonne de tri ou une direction `ORDER BY` sont des éléments de structure. Dans ce cas, une allowlist explicite est généralement plus appropriée.

allowed_sort = {
    "price": "price",
    "name": "name",
    "date": "created_at"
}

column = allowed_sort.get(sort)
if column is None:
    abort(400)

Le serveur ne transmet alors jamais directement le nom de colonne fourni par l’utilisateur ; il le traduit vers une valeur connue de l’application.

Les erreurs fréquentes avec les ORM

Les ORM réduisent fortement de nombreuses injections lorsqu’ils utilisent leurs APIs structurées. Mais presque tous offrent un moyen d’exécuter du SQL natif, des fragments ou des expressions dynamiques. Une application peut donc être sûre dans la majorité de ses requêtes et vulnérable sur une fonctionnalité de reporting ajoutée plus tard.

En white box, les appels de type raw query, native query ou les expressions construites par concaténation méritent une attention prioritaire. Il faut également examiner les fonctions qui acceptent dynamiquement des noms de colonnes, des fragments d’ordre ou des expressions de recherche avancée.

Blind SQL injection : comprendre l’oracle

Une injection reste exploitable même si la réponse de la base n’est jamais affichée directement. Il suffit parfois qu’une condition vraie et une condition fausse produisent un comportement observable différent : nombre de résultats, statut, taille ou temps de réponse.

Le pentester cherche donc un oracle, c’est-à-dire un signal lui permettant de déduire une information. L’objectif de la preuve de concept n’est pas nécessairement d’extraire toute la base ; quelques bits d’information suffisent à démontrer la capacité de lecture indirecte.

Les oracles temporels doivent être interprétés avec prudence. La latence réseau et la charge du serveur produisent naturellement des variations ; le comportement doit être répétable et comparé à plusieurs requêtes de contrôle avant de conclure.

Cas pratique de pentest : injection SQL aveugle dans un filtre

Une application fictive expose une recherche d’utilisateurs internes :

GET /admin/users?department=sales

Avec un compte de test disposant légitimement de l’accès à la fonction, le pentester observe qu’une valeur volontairement invalide produit une réponse générique, sans erreur SQL visible.

Construire une hypothèse de test

Le pentester compare des valeurs qui devraient produire des ensembles différents et observe la taille de la réponse. Il cherche ensuite à déterminer si une expression logique contrôlée influence la condition de sélection. Les tests sont volontairement limités à l’environnement autorisé et à une preuve minimale.

L’objectif est de trouver un signal stable permettant de distinguer deux états de la requête, pas de lancer immédiatement un outil d’extraction automatique.

Confirmer l’oracle sans extraction massive

Une fois qu’une condition contrôlée permet de faire varier la réponse de manière stable, il dispose d’une preuve suffisante que le paramètre influence la structure de la requête. Une extraction massive n’est pas nécessaire pour qualifier la vulnérabilité ; le rapport peut démontrer l’accès indirect à une information non fournie par l’application.

Identifier la cause dans le code

sql = "SELECT * FROM users WHERE department = '" + department + "'"

La remédiation n’est pas de filtrer les caractères utilisés dans la preuve de concept. Elle consiste à paramétrer la valeur et, lorsque la logique permet des opérateurs ou colonnes dynamiques, à les mapper vers une liste explicite.

Réduire l’impact avec le moindre privilège

Même après correction de l’injection, le compte de base doit rester limité aux opérations nécessaires. Cette mesure ne prévient pas la vulnérabilité mais réduit les conséquences d’une erreur future.

Injection de commandes système : du serveur à l’exécution de programmes

Certaines fonctionnalités utilisent des programmes externes pour convertir des documents, traiter des images, compresser des archives, générer des médias ou effectuer des opérations réseau. Le risque apparaît lorsqu’une donnée utilisateur est concaténée dans une commande interprétée par un shell.

os.system("convert " + filename + " output.png")

Le développeur souhaite uniquement transmettre un nom de fichier. Le shell reçoit pourtant une chaîne complète qu’il interprète selon ses propres règles. Une donnée peut ainsi modifier non seulement un argument mais la structure de la commande.

Fonctions particulièrement exposées

Les conversions de fichiers, générations de PDF, traitements d’images, outils multimédia et utilitaires réseau sont des candidats naturels car ils délèguent une partie du travail à des binaires ou bibliothèques complexes. Le pentester cherche surtout à comprendre si une donnée contrôlée atteint directement la ligne de commande ou un interpréteur.

Il examine également les fonctionnalités d’administration, de diagnostic ou de support qui appellent parfois des commandes système considérées comme « internes » mais exposées à travers un endpoint web.

Éliminer les dépendances dangereuses au shell

Lorsque la fonctionnalité peut être réalisée par une bibliothèque, celle-ci est généralement préférable. Si un programme externe est nécessaire, les arguments doivent être fournis via les primitives d’exécution du langage sans shell, avec une allowlist adaptée aux options autorisées et des privilèges système minimaux.

Il faut toutefois rester vigilant : l’absence de shell supprime une classe importante de problèmes mais n’empêche pas nécessairement l’utilisateur de fournir un argument que le programme lui-même interprétera comme une option dangereuse. La validation doit donc également porter sur la sémantique des paramètres.

Server-Side Template Injection (SSTI)

Un moteur de template est conçu pour combiner une structure prédéfinie avec des valeurs. Une SSTI apparaît lorsque l’utilisateur influence le template lui-même et obtient ainsi une capacité d’évaluation dans le langage du moteur.

Le cas sûr consiste à charger un template fixe puis à lui passer des variables. Le cas dangereux construit dynamiquement la chaîne de template à partir d’une entrée utilisateur avant de demander au moteur de l’interpréter.

Identifier une SSTI pendant un audit

Le pentester recherche les fonctionnalités de personnalisation : e-mails, documents, messages, pages CMS, signatures ou champs permettant d’insérer des expressions. Il commence par des expressions sans effet permettant de déterminer si le moteur évalue réellement le contenu, puis identifie le moteur et le contexte.

Le simple fait que certaines séquences soient affichées différemment ne suffit pas toujours. Le moteur peut appliquer son propre escaping ou un préprocesseur peut modifier la chaîne avant le rendu. La preuve doit établir une véritable interprétation.

Du rendu dynamique à l’exécution de code

Selon le moteur, les objets exposés et la sandbox, une SSTI peut permettre de lire des variables internes, d’appeler des méthodes ou d’atteindre une exécution de code. La preuve de concept doit rester proportionnée à l’environnement et éviter des actions destructrices.

L’impact dépend également du compte système et des secrets accessibles au processus de rendu. Le moindre privilège reste donc une défense complémentaire importante.

Sécuriser les moteurs de templates

Les données utilisateur doivent être transmises comme valeurs et ne jamais servir à composer le template. Si une fonctionnalité de template utilisateur est réellement nécessaire, elle doit être conçue comme un langage restreint avec une sandbox robuste et un modèle de menace spécifique.

Une blacklist de fonctions interdites est rarement suffisante lorsque le langage complet du moteur reste accessible. Réduire explicitement les capacités autorisées est plus robuste.

Upload de fichiers : risques, exploitation et sécurisation

Un upload fournit au serveur un objet complexe qu’il peut stocker, parser, transformer ou republier. La sécurité doit donc couvrir l’ensemble de la chaîne, depuis la réception jusqu’au téléchargement futur du fichier.

Pourquoi extension et MIME type ne suffisent pas

L’extension fait partie du nom et le `Content-Type` est généralement fourni par le client. Ces deux signaux peuvent participer au contrôle mais ne prouvent pas le format réel. Lorsque le cas d’usage le permet, le serveur doit parser le contenu attendu avec une bibliothèque adaptée ou produire une nouvelle représentation maîtrisée.

Une image de profil peut par exemple être décodée puis réencodée dans un format attendu, ce qui élimine une partie des structures inattendues présentes dans le fichier original.

Stocker les fichiers hors des zones exécutables

Un fichier utilisateur ne devrait généralement pas être placé dans un répertoire où le serveur web ou le runtime peut l’interpréter comme du code. Les noms générés côté serveur évitent également plusieurs problèmes liés aux collisions, caractères spéciaux et chemins fournis par l’utilisateur.

Le stockage doit être dissocié de l’URL publique lorsque cela est possible. Une ressource peut être identifiée par un ID métier tandis que le serveur gère lui-même son emplacement physique.

SVG, PDF et autres formats riches

Un SVG est un document XML et peut contenir différents éléments actifs. Un PDF peut embarquer des liens, formulaires, objets et autres fonctionnalités. La bonne défense dépend donc du besoin métier : si une fonctionnalité d’avatar n’a besoin que d’une image raster, accepter un format actif ajoute une surface inutile.

Réduire les formats et fonctionnalités acceptés est souvent plus sûr que tenter de nettoyer un format très riche dont l’application n’utilise qu’une fraction.

Archives, décompression et ZIP Slip

Une archive peut provoquer une consommation de ressources importante après décompression ou contenir des chemins qui sortent du répertoire d’extraction. Le serveur doit contrôler la taille totale, le nombre d’éléments et la destination finale normalisée de chaque entrée avant écriture.

Une archive de petite taille ne doit donc pas être évaluée uniquement sur sa taille compressée. Le volume final et la profondeur des structures peuvent avoir un impact important sur les ressources serveur.

Sécuriser aussi le téléchargement

Un fichier correctement validé reste sensible si n’importe quel utilisateur peut ensuite le télécharger. Les endpoints de récupération doivent appliquer les mêmes règles de propriété, tenant et partage que les autres objets métier.

Le serveur doit également choisir les en-têtes de réponse en fonction de l’usage. Certains contenus doivent être téléchargés plutôt qu’interprétés inline par le navigateur, et le type retourné ne devrait pas dépendre aveuglément du nom fourni par l’utilisateur.

Pour plus d’informations, vous pouvez consulter notre article dédié : Failles d’upload de fichiers : techniques d’exploitation et méthodes de prévention.

Path Traversal et manipulation de chemins

Une application peut exposer un paramètre de fichier sans intention de fournir un accès arbitraire au système de fichiers.

GET /download?file=report.pdf

Une implémentation naïve concatène ce nom à un répertoire de base. Le système de fichiers interprète cependant la valeur comme une partie du chemin, pas comme un identifiant logique.

Les filtres retirant simplement certaines séquences sont fragiles. Une approche plus robuste résout et normalise le chemin final puis vérifie qu’il reste strictement contenu dans la racine autorisée.

Lorsque l’architecture le permet, il est encore préférable d’exposer un identifiant logique, par exemple `/documents/452/download`, et de retrouver l’emplacement physique côté serveur.

Les liens symboliques et les chemins générés lors de l’extraction d’archives doivent également être pris en compte. La comparaison doit porter sur la destination réellement résolue, pas seulement sur la chaîne initiale.

Server-Side Request Forgery (SSRF)

Une SSRF apparaît lorsqu’un attaquant peut influencer une requête réseau réalisée par le serveur. Le risque vient du fait que ce serveur dispose souvent d’une position réseau, d’une identité machine ou de relations de confiance différentes de celles du navigateur de l’utilisateur.

Comprendre la primitive SSRF

Prenons une fonctionnalité d’import d’image :

POST /api/profile/image
Content-Type: application/json

{
  "url": "https://images.example.test/avatar.jpg"
}

Le navigateur n’accède pas directement à l’URL. Il demande au backend de la récupérer. Le pentester se demande donc immédiatement quelles destinations le backend accepte et ce qu’il peut atteindre depuis son propre réseau.

Une fonctionnalité de prévisualisation, de webhook, de génération de PDF, de récupération de média ou d’import de document peut présenter exactement la même primitive sous une forme fonctionnelle différente.

Confirmer la connexion sortante

La première étape consiste généralement à utiliser un serveur de test contrôlé et à observer la requête reçue. Cette approche permet de confirmer que la connexion part réellement de l’infrastructure cible et d’identifier la méthode, les en-têtes, les éventuelles redirections et la résolution DNS.

Cette étape permet également de distinguer un traitement purement frontend d’une récupération serveur. Une URL saisie par l’utilisateur peut parfois être téléchargée par le navigateur lui-même, ce qui change complètement le modèle de menace.

Accéder à localhost et aux services internes

Un backend exposé sur Internet peut lui-même avoir accès à des interfaces d’administration, bases, moteurs de recherche, dashboards ou services internes non routables depuis l’extérieur. Une SSRF peut transformer le serveur vulnérable en intermédiaire vers ces ressources.

L’absence d’affichage complet de la réponse n’annule pas le risque. Des différences de statut, d’erreur ou de temps peuvent déjà fournir une capacité de cartographie.

L’impact dépend fortement des relations de confiance internes. Un service interne qui considère toute connexion provenant du réseau applicatif comme légitime peut transformer une SSRF en accès beaucoup plus puissant.

SSRF aveugle

Dans une SSRF aveugle, la réponse distante n’est jamais renvoyée à l’utilisateur. Les webhooks constituent un exemple classique : l’application indique simplement qu’un appel a été effectué.

Le pentester peut confirmer la primitive grâce à un endpoint qu’il contrôle puis chercher des signaux indirects. La preuve de vulnérabilité peut donc exister même sans contenu de réponse visible.

Les délais doivent toutefois être interprétés avec prudence. Des retries, files de messages ou traitements asynchrones peuvent modifier la latence sans relation directe avec la destination.

Redirections et validation incomplète

Valider uniquement l’URL initiale est insuffisant si le client HTTP suit automatiquement les redirections. La politique de sécurité doit être réévaluée pour chaque destination effectivement contactée.

Cette nuance est importante car elle sépare le moment du contrôle de celui de l’utilisation réelle de la ressource. Un serveur public autorisé peut renvoyer vers une destination qui ne l’est pas.

DNS, changements de résolution et TOCTOU

Une architecture peut résoudre un nom une première fois afin de décider s’il pointe vers une adresse publique puis refaire une résolution lorsqu’elle ouvre la connexion. Si les deux résolutions peuvent produire des résultats différents, la décision de sécurité ne porte plus nécessairement sur l’adresse effectivement utilisée.

La défense doit donc raisonner sur la destination au moment où le socket est réellement ouvert et contrôler également les redirections, les nouvelles tentatives et les éventuelles connexions secondaires effectuées par la bibliothèque utilisée.

Services cloud et métadonnées

Les environnements cloud ont historiquement rendu certaines SSRF très critiques parce qu’une instance pouvait accéder à des services de métadonnées ou d’identité indisponibles depuis Internet. Les protections des plateformes ont évolué, mais la leçon reste identique : le serveur peut avoir accès à des ressources et identités qui n’existent pas dans le contexte de l’utilisateur.

Le pentester doit donc considérer l’environnement réel de déploiement. Une SSRF jugée faible sur une machine de développement peut avoir un impact bien différent dans une infrastructure cloud disposant de services internes et d’identités de workload.

Pourquoi les filtrages naïfs échouent

Bloquer uniquement les chaînes `localhost` ou `127.0.0.1` ne constitue pas une politique réseau. Une même destination peut être représentée de plusieurs façons ou atteinte via un nom DNS. IPv4, IPv6, loopback, plages privées et link-local doivent être considérés selon le besoin métier.

La validation doit également intervenir après la résolution et être cohérente avec la destination utilisée par la bibliothèque réseau. Un filtre appliqué à la chaîne de l’URL mais pas à l’adresse finale laisse persister une différence d’interprétation.

Concevoir une défense efficace

Lorsque la fonctionnalité n’a besoin que de quelques destinations connues, une allowlist stricte reste la solution la plus simple et la plus robuste. Si des URLs arbitraires sont réellement nécessaires, la logique doit contrôler protocole, port, résolution finale, redirections et plages interdites.

Une politique d’egress réseau peut compléter cette protection en empêchant le composant chargé de récupérer les URLs d’accéder aux ressources internes les plus sensibles. Cette défense en profondeur réduit l’impact d’une erreur applicative.

La séparation architecturale est également utile. Un service dédié à la récupération de contenu externe peut fonctionner avec peu de privilèges, sans identité cloud sensible et dans un segment réseau fortement restreint.

Cas pratique de pentest : SSRF aveugle dans un webhook

Une application SaaS permet à chaque organisation de configurer une URL de webhook. Le serveur affiche simplement un statut « delivery successful » ou « delivery failed » sans retourner la réponse distante.

Confirmer le contrôle de destination

Le pentester configure d’abord une URL appartenant à son infrastructure de test et vérifie la réception de l’appel. Il note l’adresse source, les en-têtes et le comportement lors d’une redirection contrôlée.

Cette étape confirme également si le webhook est synchrone ou traité plus tard par un worker, information importante pour comprendre la position réseau réelle du composant qui effectue la requête.

Comparer des destinations externes et internes de laboratoire

Dans l’environnement autorisé, il utilise ensuite des destinations internes de test fournies pour l’audit. Une différence stable entre port fermé, service HTTP et adresse inaccessible peut démontrer une capacité de scan indirect même sans corps de réponse.

La démonstration reste volontairement limitée à des systèmes de test. Le but est d’établir la propriété de connexion interne, pas de cartographier sans nécessité l’ensemble du réseau.

Tester les redirections

Le pentester place une redirection sur son serveur vers une destination qui devrait être interdite et vérifie si le client HTTP applique de nouveau la politique. Si la destination finale est contactée malgré le filtre initial, la protection est contournable.

Recommandation de remédiation

Le rapport doit recommander une allowlist lorsque le besoin le permet. Dans le cas contraire, la validation doit porter sur chaque destination finale et être complétée par une restriction d’egress. Le contre-test doit vérifier à la fois l’URL directe, la redirection et le comportement DNS.

Parsing de documents, XXE et désérialisation

Les parseurs de formats structurés disposent parfois de fonctionnalités bien plus puissantes que la simple conversion de texte en objets. Lorsque ces capacités ne sont pas nécessaires, elles doivent être explicitement désactivées pour les données non fiables.

Le principe général consiste à éviter qu’un format présenté comme « donnée » puisse demander au parseur de charger des ressources, instancier des types arbitraires ou déclencher des comportements qui dépassent le besoin fonctionnel.

XML External Entities (XXE)

Certaines configurations XML historiques autorisent des entités externes qui peuvent provoquer des lectures ou des requêtes réseau. La recommandation générale est d’utiliser un parseur ou un mode explicitement sécurisé pour les données non fiables et de désactiver les fonctionnalités externes lorsque le besoin métier ne les exige pas.

Le pentester identifie les endpoints recevant réellement du XML : imports, anciens services SOAP, fichiers de configuration, documents bureautiques ou formats métier. Il vérifie ensuite la configuration du parseur et les comportements réseau ou de résolution associés.

Une réponse d’erreur peut parfois révéler le parseur utilisé, mais il faut éviter de conclure à partir d’un simple message. La preuve doit établir que le document contrôle effectivement une fonctionnalité externe ou un comportement non prévu.

Désérialisation non sûre

Les formats capables de reconstruire directement des objets du langage peuvent déclencher des comportements complexes pendant la désérialisation. Pour les échanges avec des utilisateurs non fiables, il est généralement préférable d’utiliser des structures de données simples puis de convertir explicitement les champs vers les objets métier attendus.

La surface dépend du runtime, des bibliothèques présentes et des classes accessibles. Une application qui n’a besoin que d’un identifiant, d’un nom et de quelques propriétés ne devrait pas accepter un graphe d’objets arbitraires provenant du client.

YAML et parseurs riches

Le même principe s’applique aux parseurs qui supportent des tags, types ou constructeurs avancés. Si le besoin fonctionnel se limite à des chaînes, nombres, listes et objets simples, le parseur doit être configuré pour ce sous-ensemble.

Les options de type « safe load » proposées par certaines bibliothèques matérialisent précisément cette distinction entre représentation de données et construction d’objets capables d’exécuter davantage de logique.

Manipulation de l’état applicatif et confiance excessive côté client

Le navigateur appartient à l’utilisateur. Les champs masqués, valeurs désactivées, calculs JavaScript, cookies fonctionnels et paramètres générés par l’interface peuvent tous être modifiés avant d’atteindre le serveur.

Cette règle semble évidente, mais elle explique une grande partie des vulnérabilités de logique métier et d’autorisation observées en audit. Une valeur n’est pas fiable parce qu’elle a été produite par le frontend officiel : dès qu’elle traverse le client, elle doit être considérée comme contrôlable.

Champs cachés et valeurs calculées

<input type="hidden" name="price" value="499">

Le fait que l’utilisateur ne voie pas le champ n’empêche pas sa modification. Un serveur e-commerce doit recalculer le prix, les remises, taxes et frais à partir de données de référence qu’il maîtrise.

Le même principe s’applique aux statuts, niveaux d’approbation, taux de commission ou identifiants de propriétaire. Le client peut transmettre une intention ; le serveur doit déterminer la valeur autoritative.

Cookies métier et données signées

Un cookie comme `role=user` ne peut pas devenir la source de vérité des permissions. Même une valeur signée ne résout pas toutes les erreurs de conception : la signature garantit principalement que la donnée n’a pas été modifiée depuis son émission. Elle ne garantit pas que cette donnée soit encore appropriée pour prendre une décision ou qu’elle n’accorde pas trop de privilèges.

Une donnée signée peut également devenir obsolète après un changement de rôle côté serveur. Si l’application continue à faire confiance à un ancien état pendant plusieurs heures, la signature protège l’intégrité d’une information devenue fausse.

Mass assignment et auto-binding

{
  "firstname": "Alice",
  "lastname": "Martin",
  "isAdmin": true
}

Les frameworks peuvent mapper automatiquement les propriétés reçues vers un objet interne. Si cet objet contient davantage de champs que ceux que l’utilisateur est autorisé à modifier, une propriété sensible peut devenir exposée par accident.

Des DTO dédiés ou une allowlist explicite des propriétés modifiables permettent de séparer l’interface publique du modèle interne. Cette séparation améliore également la maintenabilité : l’ajout futur d’un champ sensible dans le modèle ne l’expose pas automatiquement à tous les endpoints de mise à jour.

Vulnérabilités de logique métier et détournement de workflows

Les vulnérabilités de logique métier sont parmi les plus difficiles à automatiser parce que le serveur peut exécuter exactement la requête demandée. La faille vient du fait que cette action ne devrait pas être permise dans ce contexte, cet ordre ou cet état.

Pour les identifier, il faut comprendre les invariants du processus puis s’affranchir du parcours imposé par l’interface. Le pentester ne demande pas seulement « cette requête est-elle techniquement valide ? » ; il demande « cette transition est-elle cohérente avec les règles métier ? ».

Comprendre les étapes critiques d’un processus métier

Prenons une commande composée de plusieurs étapes : création du panier, application d’une réduction, validation, paiement et confirmation. Le frontend appelle naturellement les endpoints dans cet ordre, mais le serveur doit imposer lui-même les transitions valides.

Le pentester documente les états de l’objet et les requêtes qui provoquent chaque changement. Il cherche ensuite les transitions qui sont seulement supposées, pas vérifiées. Il s’intéresse aussi aux propriétés qui deviennent immuables après une étape donnée et aux actions que certains rôles ne devraient plus pouvoir effectuer une fois l’objet validé.

Saut d’étapes

POST /order/452/validate
POST /order/452/pay
POST /order/452/confirm

Le test consiste à appeler directement la confirmation ou le paiement sans avoir terminé l’étape précédente. Si l’endpoint final ne vérifie pas l’état réel de la commande, le workflow dépend du frontend plutôt que du serveur.

Le même raisonnement s’applique aux onboarding, vérifications d’identité, approbations, réservations ou procédures de signature. Une page intermédiaire n’est pas une barrière si l’étape suivante accepte directement la requête.

Modification après validation

Un processus peut valider un montant, un destinataire ou un contenu puis offrir une seconde fonctionnalité permettant de modifier cette donnée avant exécution. Si l’approbation n’est pas recalculée, l’objet finalement exécuté n’est plus celui qui a été validé.

Ce type de faille est particulièrement critique dans les processus d’approbation, de paiement et de signature. L’invariant doit relier la décision d’approbation à une version précise de l’objet ou aux propriétés qui l’ont motivée.

Réexécution de traitements à usage unique

Une interface peut masquer un bouton après l’utilisation d’un coupon ou d’un remboursement, mais la requête reste rejouable. L’unicité doit être imposée dans l’état serveur et, si possible, protégée par une contrainte transactionnelle adaptée.

Les tokens d’invitation, validations de mail, bonus, remboursements, créations de crédit et transitions administratives méritent tous cette question : que se passe-t-il si la même opération légitime est envoyée une deuxième fois ?

Détournement de processus d’approbation

Une séparation des responsabilités peut exiger que la personne qui crée une demande ne puisse pas l’approuver. Une implémentation qui vérifie uniquement la permission `APPROVE` mais pas la relation avec l’auteur peut satisfaire le RBAC tout en violant la règle métier.

Les tests doivent donc couvrir les interactions entre rôles, pas uniquement chaque permission isolément. Un utilisateur possédant plusieurs rôles dans des contextes différents peut créer une combinaison que les concepteurs n’avaient jamais envisagée.

Cas pratique de pentest : montant modifiable après approbation

Une application de gestion des achats impose une approbation manager au-delà de 5 000 euros. Le pentester crée une demande de 4 000 euros, qui ne nécessite pas cette approbation, puis observe qu’un endpoint permet encore de modifier certaines informations avant l’émission finale.

Identifier l’invariant métier

La règle attendue est que le montant exécuté corresponde à celui ayant déterminé le niveau d’approbation. Le pentester ne cherche donc pas une injection ; il cherche à savoir si une modification ultérieure réévalue cette règle.

Il confirme d’abord le comportement normal avec plusieurs montants afin d’établir que le seuil de 5 000 euros modifie réellement le parcours d’approbation.

Tester une modification après le passage du contrôle

Dans l’environnement de test, il modifie le montant vers une valeur supérieure au seuil puis déclenche l’étape finale. Si la commande est émise sans nouvelle validation, le workflow a perdu le lien entre la décision d’approbation et l’état courant de l’objet.

Le test est ensuite reproduit avec d’autres propriétés critiques afin de déterminer si le défaut concerne uniquement le montant ou un modèle plus général de modification post-approbation.

Remédiation

La correction peut consister à figer les propriétés ayant servi à l’approbation ou à invalider automatiquement l’approbation dès qu’une propriété critique change. La décision finale doit toujours être prise sur l’état réellement exécuté.

La remédiation doit également être couverte par des tests automatisés : toute modification d’une propriété critique doit ramener l’objet dans l’état nécessitant une nouvelle approbation.

Race conditions : exploiter le temps entre deux décisions

Les race conditions apparaissent lorsque plusieurs opérations concurrentes consultent un même état avant que l’une d’elles ne l’ait modifié. Le contrôle individuel peut être parfaitement correct tout en devenant vulnérable à cause de la fenêtre temporelle entre la lecture et l’écriture.

Comprendre la fenêtre de concurrence

Prenons un portefeuille contenant 100 euros. Une demande de retrait de 80 euros lit le solde, vérifie qu’il est suffisant, crée l’opération puis met le solde à jour.

Avec une requête, le comportement fonctionne. Avec deux requêtes presque simultanées, les deux workers peuvent lire 100 avant qu’aucun n’ait enregistré le nouveau solde. Chacun conclut alors indépendamment que 100 est supérieur ou égal à 80.

Le bug ne vient pas de la condition elle-même ; il vient du fait que lecture, vérification et modification ne sont pas atomiques.

Pourquoi les tests séquentiels peuvent manquer la faille

Envoyer deux retraits l’un après l’autre produit souvent un succès puis un refus. Le pentester doit donc émettre les opérations suffisamment proches pour qu’elles entrent dans la même fenêtre de concurrence. Les outils permettant de synchroniser ou paralléliser des requêtes deviennent ici utiles, mais l’hypothèse métier reste le point de départ.

Il faut également tenir compte des traitements asynchrones. Deux requêtes HTTP peuvent être traitées rapidement mais placer des messages dans une file qui seront ensuite consommés dans un ordre différent. La concurrence doit être analysée là où l’état est réellement lu et modifié.

Autres scénarios de race condition

Les mêmes mécanismes apparaissent avec des coupons, invitations, tokens à usage unique, créations de compte, changements de rôle, votes, stocks limités ou opérations de réservation. Chaque fois qu’une règle dépend de l’état courant puis modifie cet état, la question de l’atomicité mérite d’être posée.

Une autre variante concerne les limites de quota. Plusieurs requêtes peuvent toutes constater que l’utilisateur est encore sous le seuil puis chacune consommer une unité, ce qui permet de dépasser la limite prévue.

Transactions, verrous et contraintes en base

La remédiation dépend du système. Transactions, opérations atomiques, verrous optimistes ou pessimistes et contraintes d’unicité peuvent être nécessaires. Lorsqu’un invariant peut être imposé directement par la base, une contrainte offre souvent une protection plus robuste qu’une vérification applicative réalisée avant l’écriture.

Il faut cependant éviter d’utiliser un verrou global comme réponse universelle : il peut dégrader fortement les performances. L’objectif est d’imposer l’invariant au niveau approprié avec la granularité nécessaire.

Cas pratique de pentest : coupon à usage unique

Une application permet d’appliquer un coupon de bienvenue une seule fois par organisation. En usage normal, une seconde tentative est correctement refusée.

Tester la concurrence

Le pentester prépare plusieurs requêtes identiques avec le même coupon et les déclenche simultanément dans l’environnement autorisé. Plusieurs réponses indiquent un succès et le solde de réduction final montre que le coupon a été crédité plus d’une fois.

Le scénario est reproduit un nombre limité de fois pour confirmer qu’il ne s’agit pas d’un artefact ponctuel. Le pentester évite d’accumuler inutilement des crédits ou de perturber d’autres comptes.

Comprendre la cause

Le code effectue d’abord un `SELECT` pour vérifier l’absence d’utilisation puis insère l’événement de consommation. Deux transactions peuvent franchir le `SELECT` avant que l’une n’ait inséré la ligne.

Cette structure classique « check then act » est sûre uniquement si le contrôle et l’écriture sont protégés par une primitive atomique ou une contrainte qui empêche la duplication.

Corriger l’invariant

Une contrainte d’unicité sur la combinaison organisation/coupon, associée à une transaction correcte, impose l’unicité au niveau du stockage. Le code applicatif doit ensuite gérer proprement le conflit plutôt que compter sur une vérification préalable.

Sécuriser les interfaces d’administration et les back-offices

Les back-offices concentrent souvent les opérations les plus puissantes : gestion des utilisateurs, exports, impersonation, données clients, journaux et configurations. Ils sont parfois considérés à tort comme intrinsèquement sûrs parce qu’ils sont destinés à des équipes internes.

Pourquoi les back-offices sont souvent moins protégés

Une interface interne peut utiliser un framework plus ancien, être testée moins fréquemment ou accepter davantage de contenu riche. Pourtant, elle affiche souvent des données fournies par des utilisateurs externes. Cette combinaison crée une frontière de confiance particulièrement intéressante pour un attaquant.

Le fait qu’un utilisateur final ne puisse pas accéder directement à `/admin` ne protège pas le navigateur de l’administrateur contre des données malveillantes qu’il devra consulter dans le cadre normal de son travail.

XSS visant les administrateurs

Une donnée correctement affichée comme texte dans le portail client peut être rendue comme HTML dans le back-office. Le fait qu’elle provienne de la base ne la rend pas sûre : sa source originale reste un utilisateur non fiable.

C’est une forme classique de vulnérabilité de second ordre : la donnée est acceptée dans un contexte puis devient dangereuse lorsqu’elle est réutilisée plus tard dans un autre contexte.

Fonctions d’impersonation

Une fonction « se connecter en tant que client » doit être considérée comme une capacité privilégiée. Elle devrait être limitée à certains profils, journalisée, clairement visible dans l’interface et éventuellement restreinte pour les opérations les plus sensibles.

Le fait qu’un agent puisse reproduire l’expérience d’un client ne signifie pas qu’il doit automatiquement pouvoir modifier son MFA, ses moyens de paiement ou ses secrets. Une architecture peut volontairement bloquer certaines actions pendant l’impersonation ou demander une étape supplémentaire.

Exports et accès aux données sensibles

Les fonctions d’export cumulent souvent fort volume et privilèges élevés. Elles méritent des contrôles d’accès spécifiques, une limitation de périmètre, une journalisation et parfois une réauthentification selon le niveau de sensibilité.

Le pentester vérifie également les fichiers générés : où sont-ils stockés, combien de temps restent-ils accessibles, leurs URLs sont-elles prévisibles et les contrôles d’accès s’appliquent-ils au téléchargement ?

Exposition involontaire de données sensibles

Une donnée peut être masquée par l’interface tout en étant déjà présente dans la réponse. Le pentester inspecte donc systématiquement les réponses API, objets JSON et données initiales de page plutôt que de se limiter au contenu visible.

Réponses JSON trop bavardes

{
  "firstname": "Alice",
  "lastname": "Martin",
  "company": "Example",
  "internalNotes": "VIP customer",
  "isAdmin": false
}

Si le frontend n’affiche que les trois premières propriétés, les deux dernières restent néanmoins accessibles au navigateur. L’API doit retourner uniquement les champs nécessaires au cas d’usage.

Les serializers génériques peuvent provoquer ce problème lorsqu’ils exposent automatiquement toutes les propriétés d’un objet interne. Des modèles de réponse dédiés réduisent le risque que l’ajout futur d’un champ sensible rende cette donnée visible à tous les clients.

Logs exposés ou trop bavards

Les journaux peuvent accidentellement contenir tokens, cookies, mots de passe, clés API ou données personnelles. Leur accès est également souvent plus large que celui de la base de production. La journalisation doit donc appliquer une politique explicite de masquage des secrets.

Le pentester cherche également les endpoints permettant de consulter les logs depuis l’application. Une fonction de support peut exposer beaucoup plus d’informations que ce qui était prévu initialement.

Fichiers temporaires, sauvegardes et source maps

Des fichiers comme `config.old`, `backup.zip`, exports temporaires ou source maps peuvent révéler du code, des routes, des secrets historiques ou des données. Le processus de déploiement doit supprimer les artefacts inutiles et empêcher leur exposition par le serveur web.

Une source map n’est pas nécessairement une vulnérabilité critique en elle-même. Elle peut cependant accélérer considérablement la cartographie en révélant les noms de fonctions, routes ou commentaires du code source frontend.

Cache navigateur et CDN

Une réponse authentifiée ne doit pas devenir accidentellement une ressource publique parce que son URL ressemble à un fichier statique. Les directives de cache doivent être choisies en fonction du caractère personnalisé et sensible de la réponse.

Les caches intermédiaires doivent également comprendre quelles dimensions de la requête influencent le contenu. Une réponse variant selon l’authentification, l’Host ou certains headers ne doit pas être réutilisée entre utilisateurs si la clé de cache ne représente pas ces différences.

Cryptographie applicative et gestion des secrets

Le chiffrement ne doit jamais être appliqué sans modèle de menace. TLS protège le transport. Le chiffrement au repos peut protéger certaines données en cas de vol du support ou de compromission de la base, mais son efficacité dépend de l’endroit où sont conservées les clés.

Une application qui chiffre une donnée et stocke la clé dans exactement le même contexte avec les mêmes permissions protège peu contre la compromission de ce contexte. La séparation des secrets, la rotation et la gestion des accès font donc partie intégrante de la cryptographie applicative.

Les équipes devraient utiliser des primitives et bibliothèques reconnues plutôt que concevoir leurs propres algorithmes ou protocoles. Une mauvaise composition de primitives solides peut produire un système fragile même lorsque les algorithmes sous-jacents sont réputés sûrs.

Chiffrement, intégrité et objectifs différents

Le chiffrement protège la confidentialité, tandis qu’un mécanisme d’authentification ou une signature peut protéger l’intégrité et l’authenticité. Ces objectifs ne sont pas interchangeables.

Par exemple, chiffrer une valeur stockée côté client n’empêche pas nécessairement un attaquant de remplacer tout le ciphertext par un autre ciphertext valide. Si la décision de sécurité nécessite de vérifier l’intégrité et le contexte, le mécanisme choisi doit fournir explicitement cette propriété.

Gestion et rotation des clés

Une clé cryptographique est elle-même un secret à protéger. Les clés doivent être accessibles uniquement aux composants qui en ont besoin, pouvoir être renouvelées et disposer d’un cycle de vie documenté.

La rotation ne doit pas uniquement être prévue en cas d’incident. Une architecture doit savoir comment déchiffrer ou vérifier les données produites par l’ancienne clé pendant la période de transition sans maintenir indéfiniment des secrets obsolètes.

CORS, CSP et en-têtes de sécurité HTTP

Les navigateurs fournissent plusieurs mécanismes de défense que les applications peuvent configurer. Ils sont puissants mais doivent être compris précisément : une mauvaise interprétation produit soit un faux sentiment de sécurité, soit des alertes injustifiées.

CORS

CORS détermine principalement si du JavaScript exécuté depuis une origine peut lire certaines réponses provenant d’une autre origine. Une configuration `Access-Control-Allow-Origin: *` n’implique pas automatiquement que n’importe quel site puisse lire des réponses authentifiées avec des credentials.

Les scénarios dangereux apparaissent plutôt lorsque le serveur accepte une origine contrôlée par l’attaquant et autorise les credentials.

Origin: https://evil.example.test

Access-Control-Allow-Origin: https://evil.example.test
Access-Control-Allow-Credentials: true

Une origine doit être comparée rigoureusement à une liste de valeurs de confiance. Les validations par simple sous-chaîne ou expression régulière mal conçue sont des causes fréquentes de contournement.

Réfléchir automatiquement la valeur `Origin` revient à déléguer au navigateur la décision de confiance. Les applications doivent être particulièrement prudentes avec les sous-domaines, car un sous-domaine abandonné ou contrôlé par un tiers peut transformer une règle apparemment restrictive en accès cross-origin réel.

Le pentester vérifie donc non seulement la syntaxe de la règle mais également si les origines autorisées sont réellement sous le contrôle de l’organisation et si des redirections ou reprises de sous-domaines modifient ce modèle de confiance.

Content Security Policy

CSP permet de limiter les sources depuis lesquelles le navigateur peut charger ou exécuter des ressources. Les politiques strictes utilisant des nonces ou hashes offrent généralement de meilleures propriétés que celles qui reposent largement sur `unsafe-inline` et de vastes listes de domaines.

Une CSP reste néanmoins une défense en profondeur. La correction d’une XSS consiste d’abord à supprimer la confusion entre donnée et code ; CSP limite ensuite certains chemins d’exploitation résiduels.

Une politique très permissive peut donner l’impression qu’une CSP est présente tout en laissant de nombreuses capacités d’exécution. L’audit doit donc interpréter la politique dans son ensemble et la confronter aux scripts réellement chargés, plutôt que se contenter de la présence de l’en-tête.

Les sources tierces autorisées doivent également être évaluées. Autoriser un domaine qui permet lui-même d’héberger du contenu contrôlé peut réduire fortement la valeur de la politique.

Clickjacking

Le clickjacking utilise l’intégration d’une page dans une frame pour tromper l’utilisateur sur l’élément qu’il active. La directive `frame-ancestors` de CSP permet de définir quelles origines peuvent intégrer la page. `X-Frame-Options` reste pertinent pour certains contextes et compatibilités.

L’impact dépend des actions accessibles par simple clic et du niveau de protection déjà fourni par l’interface. Les actions à fort impact devraient généralement demander des confirmations ou preuves supplémentaires indépendantes de la protection contre les frames.

Les en-têtes réellement utiles

`Strict-Transport-Security`, `X-Content-Type-Options`, `Referrer-Policy` et `Permissions-Policy` peuvent réduire certaines surfaces. Ils ne corrigent toutefois ni les autorisations, ni les injections, ni les vulnérabilités métier. Leur valeur vient de la défense en profondeur et d’un durcissement cohérent du comportement du navigateur.

WebSockets, Web Messaging et communications temps réel

Les WebSockets introduisent un canal persistant mais ne changent pas les principes d’autorisation. Authentifier l’ouverture de la connexion ne signifie pas que tous les messages qui suivent sont autorisés.

Autoriser chaque message métier

{
  "action": "subscribe",
  "conversation_id": 452
}

Le serveur doit vérifier que l’utilisateur a le droit de s’abonner à la conversation 452, exactement comme il le ferait sur un endpoint HTTP `GET /conversations/452`. Le canal permanent ne doit pas devenir un raccourci contournant les contrôles d’objets.

Le même principe vaut pour l’envoi de messages, le changement de canal, les événements d’administration et les actions déclenchées par des messages spécifiques. Les autorisations doivent être appliquées au niveau métier, pas uniquement au handshake initial.

Origine de la connexion

Selon la manière dont l’authentification est transmise, la validation de l’`Origin` peut réduire certains scénarios où un site tiers tente d’ouvrir une WebSocket depuis le navigateur d’une victime déjà authentifiée.

Cette validation ne remplace pas les contrôles d’autorisation des messages. Elle ajoute une protection contre certaines utilisations cross-site inattendues du navigateur.

postMessage et validation de l’émetteur

`window.postMessage` permet à des contextes différents de communiquer. Un récepteur doit valider l’origine réelle avant d’utiliser le contenu. Un message inter-frame est une donnée non fiable tant que la relation de confiance avec son émetteur n’a pas été établie.

Le `targetOrigin` utilisé par l’émetteur doit également être aussi précis que possible. Envoyer des données sensibles vers une destination générique augmente le risque qu’un autre contexte les reçoive.

Host Header, reverse proxies et confiance dans les en-têtes

Une requête moderne traverse souvent un CDN, un load balancer, un WAF puis un reverse proxy avant d’atteindre le serveur applicatif. Ces composants ajoutent des en-têtes permettant de transmettre l’adresse cliente, le protocole ou le nom d’hôte original.

L’application doit savoir exactement quels intermédiaires sont de confiance. Un en-tête `X-Forwarded-For` fourni directement par un utilisateur ne peut pas devenir une preuve d’adresse IP interne simplement parce qu’il porte le bon nom.

X-Forwarded-For, X-Forwarded-Proto et proxies de confiance

Le framework doit être configuré avec la liste des proxies autorisés à définir ces informations. Aux frontières de l’infrastructure, les en-têtes entrants doivent être normalisés ou remplacés afin qu’une valeur externe ne puisse pas se faire passer pour celle d’un proxy de confiance.

Cette configuration influence parfois les contrôles de sécurité, la génération de liens, les logs et la décision d’utiliser HTTPS. Une mauvaise chaîne de confiance peut donc produire plusieurs effets simultanément.

Host Header et génération de liens sensibles

Les liens de reset, d’invitation ou de callback devraient utiliser une origine canonique configurée plutôt que construire leur domaine directement à partir du `Host` reçu. Cette règle réduit les attaques de poisoning et les comportements incohérents derrière les proxies.

Le serveur peut également rejeter les noms d’hôte qui ne correspondent pas aux domaines explicitement prévus au lieu de laisser chaque fonctionnalité décider individuellement si la valeur est acceptable.

HTTP Request Smuggling et désynchronisation

Le request smuggling est une famille de vulnérabilités qui apparaît lorsque plusieurs composants HTTP ne déterminent pas de la même manière les limites d’une requête. Il faut donc comprendre l’architecture avant de mémoriser des variantes comme CL.TE ou TE.CL.

Pourquoi deux parseurs peuvent se désynchroniser

Un frontend reçoit une suite d’octets depuis le client puis transmet les requêtes à un backend, parfois sur une connexion réutilisée. Les deux composants doivent être d’accord sur l’endroit où se termine chaque message.

Si le frontend considère qu’une requête est terminée alors que le backend estime que certains octets appartiennent encore à son corps, les données restantes peuvent modifier l’interprétation de la requête suivante.

La vulnérabilité provient donc d’un désaccord de parsing. Un même flux est valide ou délimité différemment selon le composant qui l’analyse.

Content-Length et Transfer-Encoding

HTTP/1.1 possède plusieurs mécanismes permettant de délimiter un corps. Les vulnérabilités classiques apparaissent lorsque les composants ne donnent pas la même priorité ou n’interprètent pas de la même manière des en-têtes ambigus.

Un exemple conceptuel de requête ambiguë contient simultanément une longueur et un encodage de transfert. Le test précis dépend fortement des serveurs et ne doit être effectué que dans un environnement autorisé, car une désynchronisation peut affecter les requêtes d’autres utilisateurs.

Il est plus utile de comprendre la propriété recherchée que de recopier un payload universel : le pentester cherche à savoir si le frontend et le backend calculent la même frontière pour exactement le même flux.

Pourquoi l’impact peut concerner d’autres utilisateurs

Lorsque le backend réutilise une connexion, les octets que l’un des composants considère comme résiduels peuvent être préfixés à une requête ultérieure. Selon la pile, cela peut modifier le routage, le cache ou l’association entre réponse et requête.

Cette propriété explique pourquoi les tests de request smuggling nécessitent une prudence particulière : une preuve de concept mal contrôlée peut perturber un trafic partagé. Les environnements de préproduction, connexions dédiées et fenêtres de test sont donc particulièrement utiles.

HTTP/2, downgrades et conversions de protocole

L’exposition HTTP/2 au navigateur ne supprime pas automatiquement les risques. Un proxy peut convertir la requête vers HTTP/1.1 avant de la transmettre au backend. Cette conversion devient une nouvelle frontière de parsing et doit être comprise pendant l’audit.

Les différences de normalisation d’en-têtes, pseudo-headers et longueurs entre protocoles peuvent introduire de nouvelles variantes. Le pentester doit donc distinguer le protocole côté client de celui réellement utilisé entre les couches internes.

Méthodologie de test prudente

Le pentester commence par identifier la chaîne de proxies et les versions de protocole. Il utilise ensuite des requêtes de laboratoire conçues pour détecter une divergence sans provoquer volontairement la contamination de requêtes de tiers. Des marqueurs uniques, connexions dédiées et environnements de préproduction sont préférables lorsque l’architecture le permet.

Une anomalie de timeout ne suffit pas à conclure. Il faut reproduire le comportement, éliminer les artefacts réseau et confirmer qu’il existe bien une différence de parsing entre composants.

Une fois la divergence démontrée, la preuve peut rester limitée. Il n’est pas nécessaire de perturber la requête d’un autre utilisateur pour établir qu’une pile est désynchronisable.

Remédiation

La défense repose sur des composants à jour, le rejet des requêtes ambiguës, une normalisation cohérente et la maîtrise des conversions entre protocoles. Le frontend et le backend doivent appliquer une interprétation compatible et ne pas transmettre des messages dont la délimitation n’est pas claire.

Réduire le nombre de couches et éviter les transformations inutiles peut également diminuer la surface. Lorsque plusieurs composants sont indispensables, leurs règles de parsing et de normalisation doivent être testées ensemble.

Cas pratique de pentest : suspicion de désynchronisation derrière un reverse proxy

Une application de préproduction utilise un CDN puis un reverse proxy avant le serveur web. Le pentester observe des comportements de timeout inhabituels uniquement lorsqu’une requête contient certaines combinaisons d’en-têtes de délimitation.

Établir l’architecture avant de tester

Il confirme d’abord que le frontend et le backend n’utilisent pas nécessairement le même protocole et demande, lorsque c’est possible, un créneau de test ou un environnement isolé. Cette précaution évite d’impacter des utilisateurs réels.

Il vérifie également si le CDN normalise ou supprime certains headers avant de transmettre la requête, car le comportement observable depuis Internet ne reflète pas forcément celui du reverse proxy interne.

Rechercher une divergence reproductible

Le pentester utilise des marqueurs uniques et des connexions contrôlées afin de déterminer si le backend conserve réellement des octets que le frontend considère comme consommés. Une simple variation de latence n’est pas suffisante ; le comportement doit être répétable et lié à la délimitation du message.

Qualifier sans perturber le trafic

Une fois la divergence confirmée, la preuve peut s’arrêter avant toute démonstration visant une requête tierce. Le rapport décrit le risque de désynchronisation et fournit les conditions techniques observées sans nécessiter une exploitation intrusive.

Cache poisoning et cache deception

Les caches introduisent un tiers entre le client et le serveur. Une réponse générée pour une requête peut être réutilisée pour d’autres utilisateurs, ce qui rend la clé de cache et les règles de cache aussi importantes que le contenu lui-même.

Cache poisoning

Le cache poisoning apparaît lorsqu’une partie de la requête influence la réponse du backend sans être correctement représentée dans la clé du cache. L’attaquant cherche alors à faire stocker une variante contrôlée qui sera servie à d’autres utilisateurs.

Le pentester compare les headers et paramètres qui modifient la réponse avec ceux que le CDN ou reverse proxy semble utiliser pour construire sa clé. Une donnée non « keyed » qui apparaît dans la réponse mérite une analyse approfondie.

Cache deception

Le cache deception cherche plutôt à convaincre le cache qu’une réponse personnalisée peut être stockée comme une ressource publique, par exemple à cause de règles basées sur l’apparence de l’URL. Les réponses authentifiées et sensibles doivent disposer de directives cohérentes indépendamment de leur extension apparente.

La correction repose autant sur les règles de cache que sur l’application. Les chemins dynamiques et les ressources personnalisées doivent être explicitement distingués des assets réellement publics.

Sécurité des dépendances et supply chain applicative

Une application moderne repose sur un grand nombre de composants tiers. La surface d’attaque comprend les bibliothèques, dépendances transitives, registries, images de conteneurs, scripts tiers, outils de build et pipelines CI/CD.

Le risque supply chain ne se limite donc pas à « une bibliothèque possède une CVE ». Il inclut aussi la provenance, l’intégrité des artefacts, les comptes capables de publier une dépendance et les permissions des pipelines qui assemblent le produit final.

Bibliothèques vulnérables et atteignabilité réelle

Une vulnérabilité connue dans une dépendance mérite une analyse, mais son score générique ne suffit pas à déterminer l’impact réel dans l’application. Il faut savoir si la version concernée est utilisée, si le chemin vulnérable est atteignable et avec quels privilèges.

Cette analyse d’atteignabilité permet de prioriser les remédiations sans pour autant justifier de conserver indéfiniment des dépendances obsolètes. La maintenance régulière réduit aussi le coût des futures migrations de sécurité.

Dépendances transitives

Un projet peut déclarer quelques dépendances directes tout en installant des centaines de composants transitifs. L’inventaire doit donc couvrir l’arbre complet et les versions effectivement résolues, pas uniquement le manifeste de premier niveau.

Les lockfiles et SBOM peuvent aider à documenter ce qui a réellement été intégré dans une version donnée de l’application.

Dependency confusion et registries

Lorsque des packages internes et publics partagent le même espace de noms, la résolution doit être maîtrisée afin qu’un système de build ne télécharge pas automatiquement une version publique inattendue. La provenance fait partie de l’identité d’une dépendance.

Les registries internes, namespaces réservés et politiques de résolution explicites réduisent ce risque. Les pipelines doivent également vérifier qu’ils ne basculent pas silencieusement vers une source publique en cas d’indisponibilité du registry privé.

Scripts tiers exécutés dans le navigateur

Un script externe inclus dans une page s’exécute généralement avec les privilèges de l’origine qui l’intègre. La compromission du fournisseur peut donc devenir une compromission de l’application. Les scripts tiers doivent être limités, inventoriés et surveillés ; Subresource Integrity peut renforcer certains usages de ressources statiques.

Les solutions de tag management méritent la même attention, car elles peuvent permettre d’ajouter du code sans modification directe du dépôt principal.

Secrets exposés dans les pipelines et artefacts

Les pipelines manipulent souvent des secrets de déploiement, tokens de registries et clés de signature. Ils doivent éviter de les écrire dans les logs, les images finales ou les artefacts. Les jobs ne devraient recevoir que les secrets nécessaires à leur propre étape.

Les permissions du système CI/CD doivent être révisées comme celles d’une application privilégiée. Un runner capable de déployer en production constitue une cible sensible même s’il n’est jamais exposé directement aux utilisateurs finaux.

Mauvaises configurations et exposition de composants internes

Une application correctement codée peut rester vulnérable à cause de sa configuration. Modes debug, consoles de développement, endpoints de monitoring, sauvegardes, comptes par défaut, méthodes HTTP inutiles ou stockages publics peuvent exposer des capacités qui n’étaient pas prévues dans le modèle applicatif.

Environnements de staging et versions anciennes

Les environnements de test sont parfois proches de la production mais moins surveillés ou moins durcis. Un staging accessible depuis Internet doit être traité comme une véritable surface d’attaque, en particulier s’il utilise des données, secrets ou intégrations proches de la production.

Les anciennes versions oubliées sont tout aussi importantes. Une application moderne peut être correctement sécurisée alors qu’un ancien sous-domaine continue d’exposer un build vulnérable ou une interface d’administration historique.

Interfaces d’administration et monitoring

Les dashboards de framework, endpoints de santé détaillés ou consoles de monitoring peuvent révéler des informations techniques ou offrir des actions puissantes. Leur exposition doit être volontaire, authentifiée et limitée au réseau ou aux identités appropriées.

Un endpoint de healthcheck public peut fournir un statut simple sans exposer la configuration, les versions des composants ou les messages détaillés de dépendances internes.

Stockages cloud et permissions d’accès

Les buckets, conteneurs et partages de fichiers doivent être configurés selon leur usage réel. Une ressource destinée à être privée ne devrait pas dépendre uniquement du caractère difficile à deviner de son URL.

Le pentester vérifie les permissions publiques, les liens signés, leurs durées de vie et la manière dont l’application régénère ou réutilise ces liens.

Gestion des erreurs et conditions exceptionnelles

L’application est souvent conçue autour du parcours nominal. Les attaquants cherchent au contraire les états partiels, dépendances indisponibles, timeouts, valeurs extrêmes et erreurs qui peuvent faire tomber certains contrôles.

Éviter les comportements fail-open

Si un service d’autorisation devient indisponible, l’application ne devrait pas automatiquement autoriser l’action parce qu’elle ne peut plus vérifier la permission. Pour les contrôles critiques, l’échec doit généralement conduire à un refus ou à un mode dégradé explicitement sûr.

Le comportement doit être défini avant l’incident. Les choix improvisés en cas de panne tendent à privilégier la disponibilité sans toujours mesurer la perte de propriété de sécurité.

Atomicité des opérations

Une opération métier qui modifie plusieurs systèmes peut produire un état exploitable si seule une partie réussit. Paiement confirmé mais commande non enregistrée, rôle modifié mais journal non écrit ou stock décrémenté sans réservation sont des exemples où la gestion transactionnelle et les mécanismes de compensation deviennent aussi des sujets de sécurité.

Lorsque plusieurs systèmes externes sont impliqués, une transaction base de données classique ne suffit pas toujours. Les équipes doivent définir les états intermédiaires, les retries et les opérations de compensation de manière à maintenir les invariants métier.

Réponses d’erreur et fuite d’informations

Les réponses client ne devraient pas révéler inutilement des stack traces, requêtes SQL, chemins système ou secrets. Les détails nécessaires au diagnostic doivent rester dans une journalisation protégée.

Une erreur générique côté client ne doit cependant pas empêcher l’équipe technique de comprendre l’incident. La corrélation par identifiant d’erreur permet par exemple de relier un message public minimal à un log détaillé côté serveur.

Journalisation, supervision et détection

La sécurité ne consiste pas seulement à empêcher toutes les attaques. Il faut également être capable de détecter les événements qui indiquent une compromission ou un abus de privilèges.

Journaliser le contexte utile sans journaliser les secrets

Un événement de sécurité doit permettre de comprendre qui a effectué l’action, sur quelle ressource, quand, depuis quel contexte et avec quel résultat. Les mots de passe, cookies de session, access tokens et secrets ne doivent en revanche pas être enregistrés en clair.

Les changements de rôles, modifications de MFA, créations de clés API, impersonations, exports et échecs répétés d’authentification sont des exemples d’événements particulièrement utiles à tracer.

Protéger l’intégrité des logs

Des journaux stockés uniquement sur le serveur compromis peuvent être supprimés ou modifiés par un attaquant. Les événements importants gagnent donc à être centralisés vers une infrastructure séparée disposant de contrôles d’accès adaptés.

Il faut également contrôler qui peut lire les logs. Une centralisation très large peut transformer la plateforme de logging en nouvel endroit contenant des données sensibles accessibles à trop d’utilisateurs.

Détecter plutôt que seulement conserver

Une accumulation de logs sans mécanisme d’analyse apporte une valeur limitée. Les changements de rôle, désactivations de MFA, exports massifs, créations de clés API ou connexions inhabituelles peuvent déclencher des alertes proportionnées à leur criticité.

La qualité du signal est importante. Des alertes trop nombreuses et non contextualisées finissent par être ignorées. Les règles doivent privilégier les événements réellement actionnables et fournir suffisamment de contexte pour permettre une investigation rapide.

Comment un pentester audite réellement une application web

Un test d’intrusion efficace ne commence pas par l’envoi aveugle d’une bibliothèque de payloads. Il commence par la compréhension de l’application, de ses acteurs, de ses objets et des décisions de sécurité prises à chaque étape.

Les outils automatisés sont ensuite utilisés pour amplifier cette compréhension : répéter une hypothèse, comparer des réponses, cartographier un grand nombre d’objets ou identifier certains patterns. Ils ne remplacent pas le raisonnement métier.

Cette démarche est également ce qui distingue un audit de sécurité d’un simple scan de vulnérabilités. Le scanner peut signaler un header absent ou une version connue comme vulnérable ; le pentester cherche à comprendre comment l’application peut réellement être détournée dans son contexte.

Comprendre le périmètre et l’architecture

Le pentester identifie les domaines, applications, APIs et environnements inclus dans le périmètre. Il cherche également les principaux composants techniques observables : framework frontend, type d’API, mécanismes d’authentification, WebSockets, reverse proxies et services tiers.

L’objectif n’est pas encore de conclure qu’une technologie est vulnérable. Il s’agit de construire une carte suffisamment précise pour savoir où une donnée est interprétée et où une décision de sécurité peut être prise.

Dans un audit grey box ou white box, cette phase est enrichie par les informations d’architecture, dépôts de code, documentation d’API et configurations fournies. Ces informations ne remplacent pas l’observation dynamique : elles permettent de formuler plus rapidement des hypothèses pertinentes.

Parcourir l’application comme un utilisateur légitime

Avant de détourner une fonctionnalité, il faut comprendre son comportement normal. Le pentester crée des comptes, projets, documents, commandes ou tickets et observe les requêtes produites par chaque action.

Cette phase révèle les identifiants, états, paramètres cachés, appels d’API et transitions métier qui serviront de base aux hypothèses de test.

Il est utile de créer volontairement plusieurs objets comparables. Deux projets, deux documents et deux comptes fournissent des valeurs de référence qui permettront ensuite d’échanger les identifiants et de comprendre quelles relations le serveur vérifie réellement.

Cartographier les rôles et permissions

Lorsqu’il dispose de plusieurs profils, le pentester exécute les mêmes actions avec chacun et compare les requêtes. Une fonction visible uniquement pour un administrateur peut révéler un endpoint qui sera ensuite rejoué avec la session d’un utilisateur standard.

Cette comparaison horizontale et verticale est l’une des techniques les plus efficaces pour identifier les contrôles d’accès insuffisants.

Le pentester documente aussi les permissions implicites. Deux utilisateurs portant le même rôle peuvent ne pas appartenir au même tenant, département ou projet. Le contrôle horizontal ne doit donc pas être limité à la comparaison de rôles.

Cartographier les objets métier et leurs relations

Le pentester identifie les entités principales puis formalise leurs relations : un projet appartient-il à l’utilisateur ou à l’organisation ? Qui peut consulter un ticket ? Un document peut-il être partagé avec plusieurs groupes ? Quels états sont possibles ?

Une fois la relation attendue comprise, il peut chercher les endpoints qui la violent.

Cette modélisation révèle souvent des objets qui ne sont pas présentés comme tels dans l’interface. Une invitation, une clé API, un export généré ou une transaction temporaire sont aussi des objets métier avec leur propre cycle de vie et leurs propres règles d’accès.

Identifier les frontières de confiance

Chaque fonction est analysée selon la destination de ses données. Une recherche communique avec une base. Un upload atteint un parser. Un webhook déclenche une connexion. Un commentaire sera rendu dans un navigateur. Un header traverse plusieurs proxies.

Cette approche contextuelle détermine quels tests sont réellement pertinents pour chaque paramètre.

Elle permet également de repérer les vulnérabilités de second ordre. Une donnée peut être reçue à un endroit parfaitement sûr puis devenir dangereuse lorsqu’un autre composant la consomme quelques minutes ou quelques jours plus tard.

Analyser le JavaScript et les appels API

Le frontend révèle souvent des routes, paramètres, fonctionnalités historiques, rôles ou structures d’objets non visibles directement. Ces informations ne constituent pas des vulnérabilités en elles-mêmes ; elles enrichissent la cartographie et permettent de trouver des chemins à tester.

Les bundles peuvent également montrer comment les tokens sont stockés, quelles bibliothèques de rendu sont utilisées, quels endpoints sont appelés pour les fonctions d’administration et quels flags influencent l’interface.

Le pentester confronte toujours ces informations au comportement du serveur. Une condition `if (isAdmin)` dans le JavaScript ne prouve pas l’absence de contrôle backend ; elle indique seulement quelle requête mérite d’être rejouée avec un autre profil.

Tester systématiquement les autorisations

Pour chaque objet important, le pentester ne teste pas uniquement la lecture. Il compare également modification, suppression, téléchargement, partage, export et actions spécifiques.

GET /documents/453
PUT /documents/453
DELETE /documents/453
GET /documents/453/download
POST /documents/453/share

Cette couverture est essentielle car un développeur peut avoir protégé une route et oublié une variante utilisant le même objet.

Une bonne discipline consiste à construire une petite matrice pour chaque objet critique : utilisateur A sur objet A, utilisateur A sur objet B du même tenant, utilisateur A sur objet C d’un autre tenant, profil moins privilégié et profil administrateur. Cette méthode rend les lacunes beaucoup plus visibles.

Tester tous les parcours d’authentification

Le pentester analyse le login mais aussi inscription, invitation, reset, MFA, SSO, changement d’adresse et récupération. L’objectif est de déterminer si un parcours secondaire offre une preuve d’identité plus faible que le mécanisme principal.

Il s’intéresse également aux transitions entre états : session après premier facteur, session après MFA, session après changement de mot de passe, session d’impersonation et session après révocation d’un rôle.

Prioriser les fonctionnalités à fort potentiel

Uploads, imports d’URL, exports, générateurs de documents, moteurs de recherche, webhooks, paiements, fonctions d’administration et intégrations traversent souvent des frontières sensibles. Ils reçoivent donc davantage de temps sans pour autant exclure les fonctionnalités plus ordinaires.

La priorité dépend aussi du métier. Dans une application financière, les opérations de validation et de transfert seront plus critiques qu’un module de profil. Dans un SaaS documentaire, l’isolation multi-tenant et les fonctions de partage peuvent être prioritaires.

Tester les entrées dans leur contexte

Le pentester ne traite pas tous les paramètres comme des chaînes interchangeables. Une valeur utilisée dans SQL appelle une analyse d’injection. Une URL consommée par le backend appelle une analyse SSRF. Un identifiant métier appelle une analyse d’autorisation. Un chemin appelle une analyse de traversal.

Cette contextualisation explique pourquoi une approche manuelle guidée par la compréhension peut être beaucoup plus efficace qu’un fuzzing uniforme.

La même valeur peut d’ailleurs traverser plusieurs contextes. Un nom de fichier peut être stocké dans une base, réutilisé dans une commande système puis affiché dans du HTML. L’analyse doit suivre le cycle de vie complet lorsqu’un comportement inhabituel le justifie.

Analyser les workflows et les invariants métier

Après avoir reproduit le parcours normal, le pentester modifie l’ordre, rejoue les opérations, change une valeur après validation, alterne les rôles et parallélise les requêtes lorsque l’état semble sensible à la concurrence.

Il cherche les hypothèses implicites : l’utilisateur arrivera toujours depuis cette page ; cette action n’est effectuée qu’une fois ; le frontend empêche cette valeur ; posséder l’URL signifie être autorisé.

La meilleure manière de tester une logique métier consiste souvent à écrire explicitement l’invariant attendu puis à chercher toutes les requêtes capables de le rendre faux.

Utiliser l’automatisation de manière ciblée

L’automatisation devient particulièrement utile lorsqu’elle amplifie une hypothèse précise. Au lieu de demander à un outil de « tout tester », le pentester peut lui faire comparer plusieurs centaines d’identifiants entre deux sessions, rechercher les réponses divergentes ou rejouer une opération avec des variations contrôlées.

Cette approche augmente la couverture sans abandonner l’interprétation humaine du résultat.

L’automatisation est également utile pour les tests de régression pendant le contre-test. Une matrice d’autorisation déjà construite peut être rejouée rapidement après une correction afin de vérifier qu’aucune variante n’a été oubliée.

Construire des chaînes d’exploitation

Une source map peut révéler une route oubliée. Cette route peut exposer une configuration. Cette configuration peut révéler une fonctionnalité interne présentant un contrôle d’accès insuffisant. Pris isolément, chaque élément semble limité ; ensemble, ils produisent une compromission plus importante.

Le pentester doit donc rechercher les dépendances entre vulnérabilités et ne pas évaluer chaque finding dans un silo.

Il doit néanmoins distinguer clairement les faits démontrés des scénarios hypothétiques. Une chaîne est d’autant plus utile qu’elle est reproductible et qu’elle correspond réellement aux privilèges et données disponibles dans l’environnement audité.

Évaluer l’impact métier

La criticité ne dépend pas de la sophistication technique. Un IDOR trivial peut exposer toute la base client, alors qu’une injection difficile à exploiter peut ne donner accès qu’à des données peu sensibles.

L’analyse tient compte du volume, du niveau de privilège nécessaire, de l’automatisation possible, de la confidentialité, de l’intégrité, de la disponibilité et surtout de la conséquence métier réelle.

Il faut aussi distinguer la vulnérabilité d’une démonstration artificiellement maximale. Si une faille permet de consulter les factures d’un autre client, le risque est déjà établi ; il n’est pas nécessaire de télécharger toutes les factures pour mesurer son importance.

Proposer une correction de cause racine

Une remédiation de qualité ne bloque pas seulement le payload ayant servi à la démonstration. Elle restaure une propriété de sécurité : paramétrer la requête, centraliser l’autorisation, rendre l’opération atomique, valider la destination réseau finale ou lier correctement un token à sa transaction.

Le rapport doit donc expliquer suffisamment le mécanisme pour que l’équipe de développement puisse rechercher les mêmes patterns ailleurs. Un bon finding aide à supprimer une classe d’erreurs, pas seulement un endpoint vulnérable.

Contre-tester la remédiation

Le contre-test ne consiste pas uniquement à rejouer la même requête et observer un `403`. Le pentester examine ce qui a changé et teste les variantes liées à la même cause racine. Une correction d’IDOR doit être vérifiée sur toutes les actions associées à l’objet ; une correction XSS doit être évaluée dans le contexte final de rendu ; une correction SSRF doit couvrir redirections et résolution.

Le contre-test vérifie aussi les régressions évidentes. Une protection peut bloquer l’attaque mais casser le parcours légitime ou introduire une seconde route moins protégée. La qualité de la remédiation repose sur la conservation de la règle métier correcte, pas sur le simple échec de la preuve initiale.

Construire une stratégie durable de sécurité applicative

Le pentest fournit une photographie approfondie à un moment donné, mais la sécurité doit être intégrée pendant tout le cycle de vie. L’objectif est d’empêcher certaines classes de failles avant même qu’elles n’atteignent l’environnement de test.

Une stratégie mature combine conception, développement, tests, déploiement, supervision et retours d’expérience. Le pentest reste essentiel parce qu’il confronte l’ensemble à un comportement adversarial réel, mais il ne doit pas être la première fois que l’organisation se pose les questions de sécurité fondamentales.

Pendant la conception

Les équipes peuvent identifier les actifs sensibles, rôles, frontières de confiance, invariants métier et scénarios d’abus. Le threat modeling transforme une partie des hypothèses implicites en questions explicites avant l’écriture du code.

Pour chaque fonctionnalité critique, il est utile de documenter qui peut déclencher l’action, quelles données elle consomme, quelles ressources elle modifie et quelles propriétés doivent rester vraies avant et après l’opération.

Pendant le développement

Les frameworks et bibliothèques de sécurité doivent être utilisés de manière cohérente. Les autorisations centralisées, revues de code, tests automatisés, analyse de dépendances et détection de secrets permettent de réduire plusieurs erreurs récurrentes.

Les tests de sécurité les plus rentables sont souvent ceux qui formalisent les invariants métier. Une matrice d’autorisation ou un test vérifiant qu’une modification invalide une approbation empêchent des régressions qu’un linter générique ne pourra jamais comprendre.

Avant la mise en production

Le déploiement doit être vérifié au même titre que le code : configuration, secrets, exposition réseau, en-têtes, comptes techniques, logs, alertes et environnements auxiliaires. Une application sûre en développement peut devenir vulnérable par sa configuration de production.

Les différences entre staging et production doivent être connues. Un pentest réalisé sur un environnement dont les proxies, mécanismes SSO ou règles réseau diffèrent fortement de la production peut laisser des angles morts importants.

Pendant l’exploitation

Les dépendances évoluent, les fonctionnalités changent et de nouvelles surfaces apparaissent. La supervision, les mises à jour, la gestion de vulnérabilités et les pentests réguliers permettent de réévaluer les hypothèses à mesure que le système évolue.

Les incidents et findings doivent aussi alimenter le développement. Une vulnérabilité d’autorisation corrigée sur un endpoint peut devenir une règle de revue de code, un test automatisé et une exigence d’architecture pour les futures fonctionnalités.

Conclusion

Sécuriser une application web ne consiste pas à cocher une liste figée de vulnérabilités. Les technologies changent, les architectures se distribuent et de nouveaux mécanismes apparaissent, mais les causes profondes restent remarquablement stables.

Une donnée est considérée comme fiable alors qu’elle ne devrait pas l’être. Une action est autorisée uniquement parce que l’utilisateur est authentifié. Une vérification a lieu avant que l’état ne change. Deux composants interprètent différemment la même requête. Une règle métier existe dans l’interface mais n’est jamais imposée par le serveur.

Le rôle d’un pentest est précisément de confronter ces hypothèses à un comportement adverse réaliste. Le pentester cherche ce qui garantit réellement les autorisations, la séparation des données et du code, l’ordre des opérations, l’intégrité des transactions et la validité des identités.

Une remédiation durable ne bloque pas seulement la preuve de concept. Elle restaure une propriété de sécurité fondamentale : requête paramétrée, politique d’autorisation cohérente, atomicité, restriction réseau, validation complète d’un token ou séparation claire des responsabilités.

Une application réellement robuste n’est donc pas celle dans laquelle on accumule le plus de filtres. C’est celle dont les invariants restent vrais lorsque l’utilisateur modifie les requêtes, change l’ordre des opérations, automatise ses interactions ou utilise les fonctionnalités dans des conditions qui n’avaient pas été prévues initialement.

La sécurité applicative devient alors moins une collection de correctifs qu’une discipline de conception : identifier ce qui doit rester vrai, déterminer où cette propriété pourrait être violée et imposer le contrôle au bon endroit, avec des mécanismes suffisamment généraux pour résister à des variantes que les développeurs n’ont pas encore imaginées.

Références

Ce dossier s’appuie sur les principes et pratiques de référence de la sécurité applicative. Les versions indiquées ci-dessous correspondent aux publications stables vérifiées lors de la préparation de cette édition.

  • OWASP Top 10:2025 : document de sensibilisation aux principaux risques des applications web. Broken Access Control reste classé A01:2025 et l’édition 2025 met notamment en avant les erreurs de configuration, la supply chain, les défauts cryptographiques, les injections, l’insecure design, l’authentification, l’intégrité, la journalisation et les conditions exceptionnelles.
  • OWASP Application Security Verification Standard (ASVS) 5.0.0 : standard de vérification permettant de structurer les exigences de sécurité applicative et d’en faire une base de conception, de tests et de contractualisation.
  • OWASP Web Security Testing Guide (WSTG) v4.2 : version stable actuelle du guide de méthodologie de test. Le projet indique que la version 5.0 reste en développement.
  • RFC 9700 / BCP 240 : Best Current Practice for OAuth 2.0 Security, publié en janvier 2025. Il consolide les recommandations modernes de sécurité OAuth, notamment autour des redirect URIs, de PKCE et de la protection des tokens.
  • RFC 10017 / BCP 212 : OAuth 2.0 for Browser-Based Applications, publié en août 2026, consacré aux menaces et bonnes pratiques propres aux applications exécutées dans le navigateur.
  • OWASP Cheat Sheet Series : recommandations opérationnelles complémentaires sur l’authentification, les sessions, XSS, CSRF, SQL injection, SSRF, les uploads, la cryptographie et de nombreux autres contrôles applicatifs.

Ces références doivent être considérées comme des cadres complémentaires. L’OWASP Top 10 est avant tout un document de sensibilisation et ne constitue pas une checklist exhaustive. L’ASVS permet d’aller plus loin dans les exigences de contrôle, tandis que le WSTG fournit une méthodologie de test structurée. Le présent dossier cherche à relier ces cadres au raisonnement concret rencontré pendant un pentest d’application web.