Quand on apprend à coder, la sécurité arrive rarement en haut de la liste. La priorité, c'est que la fonctionnalité marche, que la page s'affiche, que la requête renvoie les bonnes données. Une faille de sécurité a une particularité gênante : elle ne casse rien pendant le développement. Le projet fonctionne parfaitement, les tests passent, la démo est propre. Le trou reste invisible jusqu'au jour où quelqu'un décide de le chercher, et ce quelqu'un est souvent un robot qui balaie des milliers de dépôts et d'URLs à la chaîne.
Ces failles ne sont pas réservées aux débutants par manque de talent. Elles apparaissent parce que les tutoriels et les cours privilégient le résultat rapide, et qu'un exemple pédagogique montre rarement la version durcie du code. On apprend à afficher une donnée, pas à se demander d'où elle vient. On apprend à connecter une base, pas à filtrer ce qu'on y injecte. La sécurité s'ajoute rarement dans le premier passage, et sur un projet personnel, personne ne vient te la réclamer.
Voici les cinq failles qui reviennent le plus souvent dans les projets d'apprentissage et les premiers dépôts publics. Pour chacune, le motif de code qui la crée, pourquoi elle est dangereuse, et la version corrigée. L'objectif reste modeste : te permettre de repérer et de fermer ces portes avant de partager un lien, que ce soit avec un recruteur, un client ou de vrais utilisateurs. Pas besoin de viser le niveau d'un expert en sécurité offensive pour ça. Chaque faille est rattachée à sa catégorie dans l'OWASP Top 10:2025, le référentiel de sécurité applicative mis à jour en novembre 2025, pour que tu puisses creuser chacune à la source.
1. L'injection SQL par concaténation
OWASP A05:2025 - Injection
C'est la faille historique, celle qui figure dans le top des classements OWASP depuis plus de vingt ans, et elle survit dans les projets débutants pour une raison simple : construire une requête en collant des morceaux de texte est la manière la plus intuitive de faire quand on découvre les bases de données.
Le motif fautif ressemble à ça :
email = request.form["email"]
query = "SELECT * FROM users WHERE email = '" + email + "'"
cursor.execute(query)
Tant que l'utilisateur tape une adresse normale, tout va bien. Le jour où il tape ' OR '1'='1 dans le champ, la requête change de sens et renvoie la table entière. Une variante plus agressive permet de vider une table, d'en lire une autre, ou de contourner complètement l'écran de connexion en forçant une condition toujours vraie. L'attaquant n'a pas besoin d'accéder à ton serveur, il lui suffit d'un formulaire et d'un peu de patience, car des outils automatisés testent ces motifs seuls sur chaque champ qu'ils trouvent.
La correction tient en un mot : requêtes paramétrées. Tu ne colles jamais une valeur saisie par un utilisateur dans le texte de la requête. Tu passes la structure d'un côté et les données de l'autre, et le pilote de base de données se charge de séparer proprement les deux.
query = "SELECT * FROM users WHERE email = %s"
cursor.execute(query, (email,))
Si tu utilises un ORM comme Doctrine, SQLAlchemy ou celui de Django, la protection est en général active par défaut, à condition de ne pas repasser en SQL brut avec des chaînes concaténées pour une requête un peu complexe. C'est justement là que la faille se réintroduit chez les gens qui pensent être protégés.
2. Les secrets commités dans Git
OWASP A02:2025 - Security Misconfiguration
Clé d'API OpenAI, mot de passe de base de données, token d'accès à un service tiers, clé secrète de signature de session. Ces valeurs finissent régulièrement dans un fichier de configuration, un settings.py ou un .env commité par mégarde, puis poussées sur un dépôt public.
Le danger ici est double. D'abord, une clé sur un dépôt public ne reste pas secrète longtemps : des chercheurs ont montré qu'une clé cloud laissée dans un repo ouvert peut être détectée et utilisée par des bots en quelques minutes, parfois moins. Ensuite, et c'est le point que la plupart des débutants ratent, supprimer le fichier dans un commit suivant ne règle rien. La valeur reste dans l'historique Git, accessible à quiconque explore les anciens commits. Un simple git log -p la fait remonter.
La bonne pratique repose sur trois réflexes. Les secrets vivent dans des variables d'environnement, jamais dans le code. Le fichier qui les contient localement est ajouté au .gitignore avant le premier commit. Et tu fournis un fichier d'exemple, souvent nommé .env.example, qui liste les clés attendues sans jamais contenir de vraie valeur.
# .gitignore
.env
.env.local
config/secrets.yaml
# .env.example (celui-ci, tu peux le commiter)
DATABASE_URL=postgresql://user:password@localhost:5432/db
OPENAI_API_KEY=sk-your-key-here
Si tu réalises qu'une clé a déjà fuité, l'ordre des opérations compte : tu révoques et régénères la clé côté service en premier, avant même de nettoyer l'historique. Supprimer le secret du dépôt et penser que le problème est réglé, alors que la clé reste valide et déjà copiée par un bot, est la faute la plus coûteuse de cette liste. Bien gérer Git dès le départ évite d'en arriver là, et c'est un sujet qu'on prend au sérieux dans notre formation Git et GitHub pour débutants.
3. Le CORS grand ouvert
OWASP A01:2025 - Broken Access Control
La scène est classique. Un front en React sur un domaine, une API sur un autre, et le navigateur bloque les requêtes avec un message qui parle de CORS. Après quelques recherches, la ligne magique apparaît sur un forum et fait disparaître l'erreur d'un coup :
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Le CORS, ou Cross-Origin Resource Sharing, est un mécanisme du navigateur qui décide quels sites extérieurs ont le droit d'appeler ton API depuis le code d'une page. Mettre une étoile revient à autoriser n'importe quel site de la planète à envoyer des requêtes vers ton service. Combiné à allow_credentials=True, ça devient un vrai problème : un site malveillant que ta victime visite peut déclencher des appels authentifiés vers ton API en utilisant ses cookies de session.
La configuration saine liste explicitement les origines qui ont réellement besoin d'accéder à l'API. En développement, c'est ton front local. En production, c'est ton nom de domaine.
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"https://mon-app.fr",
],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"],
)
Le CORS protège les utilisateurs de ton API, pas ton serveur directement, ce qui explique pourquoi il est si mal compris. Il ne remplace jamais l'authentification ni les vérifications d'accès côté serveur. Si tu construis des API, la partie sécurité fait partie des fondamentaux abordés dans notre formation API REST avec Python et FastAPI.
4. Le XSS par affichage de données non échappées
OWASP A05:2025 - Injection
Un champ commentaire, un pseudo, une bio d'utilisateur. Tu récupères la valeur saisie et tu l'affiches telle quelle sur la page. Si un utilisateur entre du texte normal, rien à signaler. S'il entre une balise script, et que ton rendu l'insère sans la neutraliser, ce script s'exécute dans le navigateur de tous les visiteurs qui voient ce contenu.
C'est le principe du Cross-Site Scripting, ou XSS. Le motif dangereux le plus fréquent en front se trouve dans l'insertion directe de HTML :
// Dangereux : le contenu utilisateur devient du HTML actif
element.innerHTML = commentaire;
// En React, le nom de la prop est un avertissement en soi
<div dangerouslySetInnerHTML={{ __html: commentaire }} />
Un attaquant peut alors voler des cookies de session, rediriger vers une fausse page de connexion ou effectuer des actions au nom de l'utilisateur connecté. La forme la plus grave est celle où le script malveillant est enregistré en base, dans un commentaire par exemple, et se déclenche ensuite chez chaque visiteur qui consulte la page. Un seul champ mal traité peut ainsi toucher tous tes utilisateurs, sans que l'attaquant ait à les cibler un par un. La correction dépend de l'endroit. En rendu texte, tu affiches la valeur comme du texte et jamais comme du HTML. Les moteurs de template modernes, Twig chez Symfony ou le JSX de React, échappent le contenu par défaut, ce qui te protège tant que tu ne forces pas l'insertion de HTML brut.
// React échappe automatiquement le contenu entre accolades
<div>{commentaire}</div>
// Twig échappe par défaut à l'affichage
{{ commentaire }}
Le jour où tu as besoin d'afficher du HTML riche fourni par un utilisateur, par exemple le rendu d'un éditeur de texte, tu passes par une bibliothèque de nettoyage comme DOMPurify plutôt que d'insérer la chaîne telle quelle. Nettoyer une entrée à la main avec des remplacements de caractères est une source d'erreurs, laisse ce travail à un outil éprouvé.
5. Le contrôle d'accès manquant
OWASP A01:2025 - Broken Access Control
Voici la faille la plus discrète des cinq, et celle qui touche le plus de projets qui ont pourtant mis en place une authentification. Tu protèges bien tes routes derrière un login, tu vérifies que l'utilisateur est connecté, et tu t'arrêtes là. Le problème est qu'être connecté ne veut pas dire avoir le droit d'accéder à cette ressource précise.
Regarde cette route qui renvoie une facture :
@app.get("/factures/{facture_id}")
def get_facture(facture_id: int, user = Depends(get_current_user)):
# L'utilisateur est bien authentifié...
return db.get_facture(facture_id)
# ...mais rien ne vérifie que la facture lui appartient
Un utilisateur connecté qui demande la facture numéro 41 peut modifier l'URL et demander la 42, la 43, la 44. Tant que rien ne vérifie que la ressource lui appartient, il consulte les factures des autres. Cette faille porte un nom, IDOR pour Insecure Direct Object Reference, et elle est particulièrement fréquente parce qu'elle ne provoque aucune erreur visible pendant les tests, où l'on utilise toujours son propre compte.
La correction consiste à vérifier l'appartenance de la ressource, systématiquement, sur chaque route qui manipule des données liées à un utilisateur :
@app.get("/factures/{facture_id}")
def get_facture(facture_id: int, user = Depends(get_current_user)):
facture = db.get_facture(facture_id)
if facture is None or facture.user_id != user.id:
raise HTTPException(status_code=404)
return facture
Renvoyer un 404 plutôt qu'un 403 quand la ressource ne t'appartient pas évite en prime de révéler qu'elle existe. Ce contrôle d'accès est le prolongement direct du principe de moindre privilège, que nous avons détaillé dans un article dédié : Least Privilege, le principe qui rend tes bugs et tes failles dix fois moins graves.
Les pièges qui reviennent le plus
Au-delà des cinq failles elles-mêmes, quelques habitudes de raisonnement les gardent en vie. En avoir conscience t'évite de les reproduire ailleurs que dans les exemples ci-dessus.
Croire que le framework protège de tout. Un ORM ou un moteur de template couvre le cas standard, puis tu repasses en SQL brut ou en HTML forcé pour un besoin précis, et la faille rentre par cette porte que tu as ouverte toi-même.
Tester uniquement avec son propre compte. Le contrôle d'accès manquant ne se voit jamais quand tu es le seul utilisateur. Crée un deuxième compte et essaie d'accéder aux données du premier, c'est le test qui révèle l'IDOR en trente secondes.
Confondre supprimer et révoquer. Retirer un secret d'un fichier ne le retire ni de l'historique Git ni de la mémoire des bots qui l'ont déjà copié. La seule action qui compte, c'est de régénérer la clé côté service.
Repousser la sécurité à plus tard. Ces réflexes coûtent quelques minutes quand on les prend dès le début, et deviennent un chantier une fois le projet en production. Les intégrer à ta manière de coder les rend indolores.
Aucune de ces failles ne demande un niveau avancé pour être corrigée. Elles demandent surtout de savoir qu'elles existent et de prendre l'habitude de les vérifier, au même titre que tu vérifies qu'une page s'affiche. Un bon réflexe pour aller plus loin : chaque fois que ton code reçoit une donnée venue de l'extérieur, demande-toi ce qui se passe si cette donnée est hostile. Si tu veux muscler cette culture de la vérification, notre article sur les tests fiables et notre formation Symfony 7 abordent la question du côté backend.