Aller au contenu principal

Les 5 failles de sécurité qu'on retrouve dans presque tous les projets juniors

Un portfolio en ligne, une petite API, un side-project posé sur un VPS. Le code tourne, la démo passe, le recruteur clique sur le lien. Et pourtant, une bonne partie de ces projets contient au moins une faille qu'un scanner automatisé repère en quelques minutes, sans qu'un humain ait besoin de regarder.

Guides & tutoriels ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

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.

Questions fréquentes

Faut-il connaître ces failles pour un premier emploi de développeur ?

Savoir qu'elles existent et pouvoir les corriger sur un projet simple est déjà un vrai signal en entretien. Un recruteur qui regarde ton dépôt et n'y voit ni secret commité ni CORS ouvert en tire une bonne impression, parce que ces détails montrent une hygiène de travail que beaucoup de candidats n'ont pas encore.

Comment vérifier si j'ai commité un secret par erreur ?

Tu peux parcourir l'historique avec git log et rechercher des mots comme key, token ou password. Des outils gratuits comme gitleaks ou trufflehog scannent un dépôt entier et signalent les secrets présents dans les anciens commits. Si tu en trouves un, régénère la clé concernée côté service avant toute autre action.

Un ORM me protège-t-il totalement de l'injection SQL ?

Il te protège tant que tu passes par ses méthodes normales, qui utilisent des requêtes paramétrées en interne. La faille revient dès que tu écris du SQL brut en collant des variables dans la chaîne, une pratique courante pour les requêtes complexes que l'ORM ne couvre pas directement. Dans ce cas, tu dois utiliser des paramètres liés, même en SQL manuel.

Le CORS suffit-il à sécuriser une API ?

Non. Le CORS gère uniquement quels sites, dans un navigateur, ont le droit d'appeler ton API depuis le code d'une page. Il n'a aucun effet sur les requêtes envoyées directement, par exemple avec curl ou un script. La vraie protection de ton API repose sur l'authentification et les vérifications d'accès côté serveur, le CORS n'étant qu'une couche complémentaire pour les navigateurs.

Par où commencer si je découvre la sécurité web ?

Le classement OWASP Top 10 est la référence gratuite pour connaître les catégories de failles les plus répandues. Une fois familier avec les noms, applique la vérification sur tes propres projets, une faille à la fois. Un article comme celui sur le principe de moindre privilège te donne un cadre de raisonnement transversal qui limite les dégâts même quand une faille passe entre les mailles.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous