Chapitre 416 minIntermédiaire
Mauvaise configuration (A02), environnements et secrets
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- repérer les réglages dangereux laissés en production ;
- séparer et isoler les environnements de développement, de test et de production ;
- ranger un secret hors du code avec un fichier .env et un coffre-fort ;
- réagir dans le bon ordre quand un secret a fuité dans Git.
3 exercices corrigés
Une mauvaise configuration de sécurité (Security Misconfiguration, A02:2025) est un réglage dangereux laissé dans un composant de l'application : compte par défaut, mode débogage, message d'erreur trop détaillé, fichier sensible accessible, service inutile. Elle monte au deuxième rang du Top 10 en 2025. Ce chapitre montre les erreurs les plus fréquentes, comment séparer et isoler les environnements, et comment gérer les secrets (mots de passe de base, clés d'API) sans jamais les écrire dans le code.
Ce qu'est une mauvaise configuration
Le code peut être irréprochable et l'application vulnérable, parce que le serveur, le framework, la base ou le conteneur qui l'exécutent sont mal réglés. L'OWASP range dans A02 notamment :
- des fonctionnalités inutiles activées : ports, services, pages d'exemple, comptes, privilèges ;
- des comptes et mots de passe par défaut jamais changés ;
- des erreurs non gérées de façon centralisée, qui affichent des traces d'exécution ou des messages trop bavards ;
- des réglages par défaut non sûrs dans les serveurs, frameworks, bibliothèques ou bases ;
- des en-têtes de sécurité absents ou mal réglés (chapitre 9) ;
- des cookies sans les attributs
SecureouHttpOnly; - un analyseur XML qui accepte les entités externes (XXE).
Comptes par défaut, pages et fichiers oubliés
Les robots qui parcourent Internet testent en permanence les mêmes adresses. Voici quelques lignes du journal d'accès d'AppliFrais, dix minutes après sa mise en ligne :
198.51.100.23 - - [12/Mar/2026:09:12:40 +0100] "GET /.env HTTP/1.1" 404 153 "-" "Mozilla/5.0 zgrab/0.x"
198.51.100.23 - - [12/Mar/2026:09:12:41 +0100] "GET /.git/config HTTP/1.1" 404 153 "-" "Mozilla/5.0 zgrab/0.x"
198.51.100.23 - - [12/Mar/2026:09:12:41 +0100] "GET /phpmyadmin/ HTTP/1.1" 404 153 "-" "Mozilla/5.0 zgrab/0.x"
198.51.100.23 - - [12/Mar/2026:09:22:30 +0100] "GET /wp-login.php HTTP/1.1" 404 153 "-" "Mozilla/5.0 zgrab/0.x"
Ici tout renvoie 404 : la configuration est saine. Sur un serveur mal réglé, chacune de ces adresses est une catastrophe.
| Élément exposé | Ce que l'attaquant obtient |
|---|---|
/.env | Les mots de passe de la base, les clés d'API, le secret de signature des jetons |
/.git/ | Tout le code source et son historique, y compris les secrets commités puis « supprimés » |
/phpmyadmin/, /adminer.php | Une interface d'administration de la base, à attaquer par force brute |
phpinfo.php | La configuration complète de PHP, les chemins, les modules, les variables d'environnement |
backup.sql, site.zip, config.php.bak | Une copie de la base ou du code, lisible en clair |
| Listage de répertoire activé | La liste de tous les fichiers d'un dossier, justificatifs compris |
Compte admin / admin sur un outil | Un accès complet, sans même chercher de faille |
Les parades : la racine du serveur web ne contient que les fichiers publics (le dossier public/ d'un projet Laravel ou Symfony, le dist/ d'un front compilé) ; les fichiers cachés sont refusés ; le listage est désactivé ; les outils d'administration ne sont pas exposés sur Internet ; tous les mots de passe par défaut sont changés à l'installation.
Messages d'erreur trop bavards
Une erreur en production ne doit révéler à l'utilisateur ni la pile d'appels, ni la requête SQL, ni les chemins des fichiers, ni les versions des logiciels. Ces informations aident l'attaquant à préparer une injection ou à chercher une CVE.
Error: ER_BAD_FIELD_ERROR: Unknown column 'mdp' in 'where clause'
at PromiseConnection.execute (/srv/appli-frais/node_modules/mysql2/promise.js:112:22)
at connexion (/srv/appli-frais/src/routes/auth.js:18:31)
En quelques lignes, l'attaquant apprend le SGBD, le nom d'une colonne, l'arborescence du serveur et la bibliothèque utilisée. Les réglages de production :
| Plateforme | Réglage |
|---|---|
| Node.js / Express | NODE_ENV=production ; un gestionnaire d'erreurs global qui renvoie un message générique (chapitre 12) |
PHP (php.ini) | display_errors = Off, log_errors = On, expose_php = Off |
| Laravel | APP_DEBUG=false dans le .env de production |
| Symfony | APP_ENV=prod ; jamais le profiler en production |
| ASP.NET Core | Pas de UseDeveloperExceptionPage() hors de l'environnement Development |
Le détail de l'erreur ne disparaît pas : il part dans les journaux du serveur, où le développeur le retrouve.
Séparer les environnements
Une application passe par plusieurs environnements, chacun avec son rôle. Le référentiel du BTS insiste sur ce point : les environnements de développement, de test et de production doivent être isolés.
| Environnement | Rôle | Données | Règles |
|---|---|---|---|
| Développement | Écrire et essayer le code, sur le poste du développeur | Jeu d'essai fictif | Débogage autorisé ; aucun secret de production |
| Test / intégration | Exécuter les tests automatisés et les tests de recette | Jeu d'essai ou données anonymisées | Réinitialisable à volonté |
| Préproduction (staging) | Valider une version dans des conditions proches du réel | Données anonymisées | Même configuration que la production, secrets différents |
| Production | Servir les vrais utilisateurs | Données réelles | Débogage interdit ; accès restreint et journalisé |
Trois règles en découlent :
- Jamais de données réelles en développement ou en test. Une copie de la base de production sur le portable d'un stagiaire est une fuite de données personnelles qui attend son heure. On anonymise (noms remplacés, RIB supprimés, montants brouillés) ou on génère des données fictives.
- Des secrets différents dans chaque environnement. Le mot de passe de la base de test ne doit ouvrir que la base de test.
- Une configuration identique et automatisée. Les écarts entre préproduction et production sont des failles en puissance. Les fichiers de configuration, les
Dockerfileet les scripts de déploiement sont versionnés et rejoués, pas recopiés à la main.
Isoler l'application
L'isolation limite ce qu'un attaquant peut faire s'il prend pied dans un composant.
- Un compte système dédié. L'application tourne sous un utilisateur sans privilèges (
appfrais), jamais sousroot. Si une faille permet d'exécuter des commandes, elles s'exécutent avec ces droits réduits. - Un compte de base de données dédié, avec les seuls droits nécessaires (droits SQL et moindre privilège).
- Des conteneurs. Chaque service dans son conteneur, avec un utilisateur non
root, un système de fichiers en lecture seule quand c'est possible, et seulement les ports nécessaires publiés. La base n'a aucun port ouvert vers l'extérieur : seule l'API la joint, sur un réseau Docker interne. - La segmentation réseau. L'API dans une DMZ, la base dans un réseau interne, un pare-feu entre les deux qui n'ouvre que le port 3306 depuis l'API (cours Cybersécurité des infrastructures).
Un extrait de compose.yaml pour AppliFrais :
services:
api:
build: .
user: "1000:1000" # pas root dans le conteneur
read_only: true
environment:
NODE_ENV: production
env_file: .env.production # secrets hors de l'image
ports:
- "127.0.0.1:3000:3000" # derrière le reverse proxy uniquement
networks: [interne]
db:
image: mysql:8.4
volumes: [donnees:/var/lib/mysql]
networks: [interne] # aucun port publié : invisible depuis l'extérieur
networks:
interne:
volumes:
donnees:
Le cours Docker détaille la construction d'images et les réseaux.
Durcir le serveur web
Le serveur web ou le reverse proxy placé devant l'API (nginx, Apache, Caddy) se règle aussi. Un extrait de configuration nginx :
server {
listen 443 ssl;
server_name frais.gsb.fr;
root /srv/appli-frais/public;
server_tokens off; # ne pas afficher la version de nginx
autoindex off; # pas de listage des répertoires
client_max_body_size 10m; # taille maximale d'un envoi (justificatifs)
location ~ /\. { deny all; } # refuse .env, .git, .htaccess…
location /api/ {
proxy_pass http://127.0.0.1:3000;
}
}
Les en-têtes de sécurité (HSTS, CSP, X-Content-Type-Options…) se posent au même endroit ou dans l'application : le chapitre 9 les présente un par un.
XXE : les entités externes XML
Un fichier XML peut déclarer des entités externes qui pointent vers un fichier ou une URL. Un analyseur mal configuré les résout :
<?xml version="1.0"?>
<!DOCTYPE note [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<note><libelle>&xxe;</libelle></note>
Si AppliFrais importe des notes de frais au format XML et affiche le libellé, il affiche le contenu de /etc/passwd. C'est l'injection d'entité externe XML (XXE, CWE-611), classée en A02 car il s'agit d'un réglage par défaut dangereux. La parade : désactiver la résolution des entités externes et des DTD dans l'analyseur (c'est le comportement par défaut de PHP depuis la version 8.0 avec libxml 2.9 et plus, mais pas de tous les analyseurs Java ou .NET anciens), ou préférer JSON.
La gestion des secrets
Un secret est une information qui donne un accès : mot de passe de la base, clé d'API d'un service de paiement ou d'envoi de mails, secret de signature des jetons JWT, clé de chiffrement, clé privée d'un certificat. La règle d'or : un secret n'est jamais écrit dans le code source.
// VULNÉRABLE : le mot de passe part dans Git, dans chaque copie du projet, dans chaque image Docker
const pool = mysql.createPool({ host: '10.0.1.10', user: 'app_frais', password: 'Gsb2026!', database: 'gsb_frais' });
Un dépôt est copié sur chaque poste de l'équipe, sur la plateforme d'hébergement, dans les sauvegardes, parfois rendu public par erreur. Des robots parcourent GitHub en continu à la recherche de clés : une clé de cloud publiée par erreur peut être exploitée en quelques minutes.
La pratique courante est la variable d'environnement, lue au démarrage :
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
En développement, les variables viennent d'un fichier .env à la racine du projet. Node.js le lit nativement depuis la version 20.6 (node --env-file=.env src/server.js) ; en PHP, la bibliothèque vlucas/phpdotenv joue ce rôle, et Laravel et Symfony l'intègrent.
| Fichier | Contenu | Versionné ? |
|---|---|---|
.env | Les vraies valeurs de ton poste | Non : listé dans .gitignore |
.env.example | Les noms des variables, avec des valeurs vides ou factices | Oui : il documente la configuration attendue |
.gitignore | Contient la ligne .env (et .env.* sauf .env.example) | Oui |
# .gitignore
.env
.env.*
!.env.example
En production, les secrets ne sont pas dans un fichier du dépôt non plus. Selon l'hébergement : variables d'environnement définies sur la plateforme, secrets de l'orchestrateur (Docker, Kubernetes), ou coffre-fort de secrets (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager) qui stocke les secrets chiffrés, contrôle qui les lit, journalise chaque accès et facilite leur rotation. Dans GitHub Actions, les secrets du dépôt (${{ secrets.DB_PASSWORD }}) sont masqués dans les journaux d'exécution.
Un secret a fuité dans Git : que faire ?
Tu as commité le .env avec le mot de passe de la base, puis tu l'as supprimé dans le commit suivant. Le mot de passe est toujours dans l'historique : git log -p le montre à qui clone le dépôt.
L'ordre des actions compte :
- Révoquer et remplacer le secret, tout de suite. Change le mot de passe de la base, régénère la clé d'API. C'est la seule action qui supprime le risque : le secret a pu être copié dès qu'il a été poussé.
- Vérifier dans les journaux qu'il n'a pas été utilisé entre-temps.
- Seulement ensuite, nettoyer l'historique si nécessaire (
git filter-repo, BFG Repo-Cleaner) et prévenir l'équipe, qui devra recloner. - Ajouter le fichier au
.gitignoreet installer un garde-fou pour la prochaine fois.
Les garde-fous automatiques :
- gitleaks recherche des secrets dans l'historique d'un dépôt (
gitleaks git .) ou dans des fichiers (gitleaks dir .), et peut tourner en hook de pré-commit pour bloquer le commit fautif ; - GitHub secret scanning détecte les formats de clés connus dans les dépôts, et sa protection à l'envoi (push protection) refuse un
git pushqui contient une clé reconnue.
Le chapitre 14 les branche dans l'intégration continue.
La checklist avant la mise en production
| Point | Vérification |
|---|---|
| Mode débogage | Désactivé (NODE_ENV=production, APP_DEBUG=false, display_errors=Off) |
| Erreurs | Message générique pour l'utilisateur, détail dans les journaux |
| Comptes | Aucun compte ni mot de passe par défaut ; comptes de test supprimés |
| Fichiers | Racine web limitée aux fichiers publics ; .env, .git, sauvegardes inaccessibles |
| Outils | phpMyAdmin, consoles, routes de débogage absents ou non exposés |
| Secrets | Hors du code, différents de ceux de test, accès restreint |
| Comptes techniques | Application et base sous des comptes sans privilèges inutiles |
| Réseau | Seuls les ports nécessaires ouverts ; base non exposée |
| HTTPS | Certificat valide, redirection de HTTP vers HTTPS, HSTS |
| En-têtes et cookies | En-têtes de sécurité posés ; cookies Secure, HttpOnly, SameSite |
| Versions | Serveur, framework et dépendances à jour |
Exercice 1 sur 3 Que révèle cette erreur ? Réussi À revoir
En saisissant une apostrophe dans le champ de recherche, un testeur obtient cette page :
Fatal error: Uncaught PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064
You have an error in your SQL syntax near ''' at line 1 in /var/www/html/frais/recherche.php:14
- Liste quatre informations que l'attaquant en tire.
- Quelles catégories du Top 10 2025 sont concernées ?
- Quels réglages et quelles corrections proposes-tu ?
Voir la réponseMasquer la réponse de l'exercice 1
- Le SGBD (MySQL ou MariaDB, erreur 1064), le langage et la bibliothèque (PHP, PDO), le chemin du fichier sur le serveur (
/var/www/html/frais/recherche.php) et surtout le fait que l'apostrophe casse la requête : elle est construite par concaténation, donc vulnérable à l'injection SQL. - A02:2025 (erreur affichée à l'utilisateur, configuration de production absente), A05:2025 (injection SQL) et, plus largement, A10:2025 (exception non attrapée).
- En configuration :
display_errors = Offetlog_errors = Ondansphp.ini, un gestionnaire d'erreurs qui affiche un message générique. Dans le code : une requête préparée (chapitre 7). Les deux sont nécessaires : masquer l'erreur ne supprime pas l'injection.
Tu avais trouvé ?
Exercice 2 sur 3 Ranger les secrets Réussi À revoir
Un étudiant pousse son projet sur GitHub avec un fichier config.js qui contient export const JWT_SECRET = 'secret'; et le mot de passe de sa base.
- Cite deux problèmes distincts dans cette ligne.
- Donne, dans l'ordre, les actions à mener maintenant que le dépôt est public.
- Réécris la configuration correctement, avec les fichiers à créer.
Voir la réponseMasquer la réponse de l'exercice 2
- Le secret est dans le code, donc dans Git et dans toutes les copies du dépôt ; et il est faible :
secretse devine, ce qui permet de forger des jetons JWT valides (n'importe qui peut devenir administrateur). - D'abord changer le secret JWT (tous les jetons existants deviennent invalides, c'est voulu) et le mot de passe de la base ; vérifier les journaux ; ensuite seulement nettoyer l'historique Git et ajouter les garde-fous (
.gitignore, gitleaks, protection à l'envoi). - Le code lit
process.env.JWT_SECRETetprocess.env.DB_PASSWORD. Un fichier.env, non versionné, contient les vraies valeurs, avec un secret JWT long et aléatoire (par exemple 64 caractères hexadécimaux générés parnode -e "console.log(crypto.randomBytes(32).toString('hex'))"). Un fichier.env.example, versionné, liste les noms des variables sans valeur. Le.gitignorecontient.env.
Tu avais trouvé ?
Exercice 3 sur 3 Isoler les environnements Réussi À revoir
Une équipe teste AppliFrais sur une copie complète de la base de production, avec le même compte MySQL root que la production. Énumère les risques et propose une organisation correcte.
Voir la réponseMasquer la réponse de l'exercice 3
Risques : des données personnelles réelles (noms, RIB, déplacements) se retrouvent sur des postes et des serveurs moins protégés, ce qui constitue une violation potentielle du RGPD ; un test qui supprime ou modifie des lignes peut viser la production par erreur, puisque le même compte y donne accès ; le compte root a tous les droits, donc une erreur ou une faille en test peut tout détruire ; une fuite du mot de passe de test compromet la production.
Organisation : une base de test distincte, remplie de données fictives ou anonymisées ; un compte dédié par environnement avec les seuls droits nécessaires (jamais root pour l'application) ; des secrets différents ; la production accessible seulement aux personnes et aux serveurs qui en ont besoin, avec des accès journalisés.
Tu avais trouvé ?