Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1227 minIntermédiaire

Journalisation (A09) et gestion des erreurs (A10) : analyser les logs

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • choisir les événements à journaliser, et ceux à ne jamais écrire ;
  • lire un journal d'accès et y repérer une attaque ;
  • analyser des connexions avec GROUP BY et HAVING ;
  • faire échouer un contrôle en refusant (fail closed) ;
  • mettre en place un gestionnaire d'erreurs global.

4 exercices corrigés1 schéma

La journalisation consiste à enregistrer, dans des journaux (logs), les événements qui permettent de détecter une attaque, d'enquêter après un incident et de prouver qui a fait quoi. Ce chapitre couvre deux catégories du Top 10 2025 : les défaillances de journalisation et d'alerte (A09), et la mauvaise gestion des conditions exceptionnelles (A10), nouvelle en 2025. Il montre quoi journaliser, comment écrire un journal exploitable, comment analyser les connexions à la main et en SQL, et comment gérer les erreurs sans ouvrir de faille.

Pourquoi journaliser

Une attaque qui n'est pas vue dure. Sans journaux, une intrusion peut rester invisible pendant des mois, et quand on la découvre, on ne sait ni par où l'attaquant est entré, ni ce qu'il a lu, ni s'il est encore là. Les journaux servent à :

  • détecter : repérer une force brute, une énumération, une injection en cours ;
  • enquêter : reconstituer le déroulement d'un incident, mesurer son étendue (quelles données, quels comptes), répondre aux obligations de notification du RGPD ;
  • prouver : le critère P de DICP. Savoir qui a validé une fiche, qui a consulté un RIB, et pouvoir le démontrer.

L'OWASP a renommé la catégorie A09 en 2025 pour insister sur l'alerte : un journal que personne ne lit ne protège rien. Il faut que certains événements préviennent quelqu'un.

Quels événements journaliser

ÉvénementExemple dans AppliFraisPourquoi
Connexions réussies et échouéesconnexion, mdurand, echecForce brute, credential stuffing, accès inhabituels
Déconnexions, expirations, changements de mot de passechangement_mdp, lmartinPrise de contrôle d'un compte
Échecs d'autorisation (403, 404 sur la ressource d'un autre)lecture_fiche, 1188, refuseTentatives d'IDOR, énumération
Actions sensibles réussiesvalidation_fiche, 1187, par agarciaTraçabilité, preuve
Consultation de données sensibleslecture_rib, id_visiteur=17Contrôle des accès aux données personnelles
Changements de droits et de comptescreation_compte, changement_roleÉlévation de privilèges
Erreurs de validation en série, erreurs serveur400 répétés, exceptionsInjection en cours, dysfonctionnement
Démarrage, arrêt, changement de configurationconfig, NODE_ENVModification non autorisée

Ce qu'il ne faut jamais écrire dans un journal : les mots de passe (même erronés : une faute de frappe révèle souvent le vrai mot de passe), les jetons de session, les jetons JWT et de réinitialisation, les clés d'API, les numéros de carte bancaire, et plus largement les données personnelles qui ne sont pas nécessaires. Un journal est lu par plus de personnes que la base, conservé ailleurs, sauvegardé : une donnée sensible qui y entre fuit.

Un bon journal

La CNIL, dans sa recommandation de 2021 sur la journalisation, attend que chaque trace permette d'identifier individuellement l'auteur, l'horodatage, l'équipement utilisé et la nature de l'opération. Une ligne utile répond à : qui, quand, d'où, quoi, sur quelle ressource, avec quel résultat.

ChampExemple
Horodatage précis, avec fuseau2026-03-12T09:21:09.412+01:00
Auteuridentifiant de l'utilisateur (17) ou login (lmartin)
Origineadresse IP, identifiant de l'équipement
Actionlecture_fiche
Ressourcefiche:1188
Résultatrefuse
Identifiant de requêtepermet de relier la ligne aux autres traces de la même requête

On préfère un journal structuré (une ligne JSON par événement) à une phrase libre : il se filtre et s'agrège facilement, et un login piégé contenant un retour à la ligne ne peut pas fabriquer une fausse ligne. Avec la bibliothèque pino en Node.js :

js
import pino from 'pino';

const journal = pino({
  redact: ['req.headers.authorization', 'req.headers.cookie', 'mdp'],   // masque ces champs
});

export function journaliserSecurite(evenement, req, details = {}) {
  journal.info({
    evenement,
    utilisateur: req.utilisateur?.id ?? null,
    ip: req.ip,
    route: `${req.method} ${req.originalUrl}`,
    ...details,
  });
}

// Dans la route de lecture d'une fiche, en cas de refus :
journaliserSecurite('acces_refuse', req, { ressource: `fiche:${req.params.id}` });
json
{"level":30,"time":1773303669412,"evenement":"acces_refuse","utilisateur":17,"ip":"10.0.2.18","route":"GET /api/fiches/1188","ressource":"fiche:1188"}

Les journaux applicatifs peuvent aussi aller dans une table de la base, comme dans plusieurs sujets d'E7. La table journal d'AppliFrais :

sql
CREATE TABLE journal (
  id INT AUTO_INCREMENT PRIMARY KEY,
  date_heure DATETIME NOT NULL,
  login VARCHAR(50),
  ip VARCHAR(45) NOT NULL,           -- 45 caractères : la longueur maximale d'une adresse IPv6 écrite en texte
  action VARCHAR(30) NOT NULL,
  ressource VARCHAR(50),
  resultat ENUM('succes', 'echec') NOT NULL
);

Protéger les journaux. Un attaquant qui a pris pied sur le serveur cherche à effacer ses traces. Les journaux doivent donc être envoyés vers un serveur séparé (serveur syslog, plateforme de centralisation), avec des droits en écriture seule pour l'application (le compte SQL de l'application a INSERT sur journal, mais ni UPDATE ni DELETE), et des accès en lecture limités et eux-mêmes tracés. Les serveurs doivent être à l'heure (NTP), sinon les traces de plusieurs machines ne se recoupent pas.

Les conserver le temps qu'il faut. La CNIL recommande une durée de conservation des journaux comprise entre six mois et un an, sauf obligation légale ou besoin justifié (un incident en cours d'enquête, par exemple). Les journaux contiennent des données personnelles (logins, adresses IP) : ils ne servent qu'à la sécurité, pas à surveiller l'activité des salariés.

Lire un journal d'accès

Le serveur web (nginx, Apache) enregistre chaque requête dans son journal d'accès, au format dit combined :

text
10.0.2.31 - - [12/Mar/2026:08:48:02 +0100] "GET /api/fiches?mois=202603 HTTP/1.1" 200 1893 "https://frais.gsb.local/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
PartieValeurSens
110.0.2.31Adresse IP du client
2 et 3- -Identité et utilisateur HTTP (rarement utilisés)
4[12/Mar/2026:08:48:02 +0100]Date et heure, avec le décalage horaire
5"GET /api/fiches?mois=202603 HTTP/1.1"Méthode, chemin et paramètres, protocole
6200Code de statut de la réponse
71893Taille de la réponse en octets
8"https://frais.gsb.local/"Page d'origine (Referer)
9"Mozilla/5.0 (…)"Logiciel client (User-Agent)

Voici un extrait du journal d'accès d'AppliFrais, le 12 mars 2026 au matin :

text
10.0.2.31 - - [12/Mar/2026:08:47:30 +0100] "POST /api/connexion HTTP/1.1" 200 412 "https://frais.gsb.local/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
10.0.2.31 - - [12/Mar/2026:08:48:02 +0100] "GET /api/fiches?mois=202603 HTTP/1.1" 200 1893 "https://frais.gsb.local/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
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"
203.0.113.80 - - [12/Mar/2026:09:20:11 +0100] "GET /api/fiches?mois=202603%27%20UNION%20SELECT%20login,mdp_hash%20FROM%20utilisateur--%20 HTTP/1.1" 400 61 "-" "sqlmap/1.9"
203.0.113.80 - - [12/Mar/2026:09:20:12 +0100] "GET /api/justificatifs?fichier=..%2F..%2F..%2Fetc%2Fpasswd HTTP/1.1" 400 61 "-" "sqlmap/1.9"
10.0.2.18 - - [12/Mar/2026:09:21:05 +0100] "GET /api/fiches/1187 HTTP/1.1" 200 734 "https://frais.gsb.local/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)"
10.0.2.18 - - [12/Mar/2026:09:21:09 +0100] "GET /api/fiches/1188 HTTP/1.1" 404 30 "https://frais.gsb.local/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)"
10.0.2.18 - - [12/Mar/2026:09:21:10 +0100] "GET /api/fiches/1189 HTTP/1.1" 404 30 "https://frais.gsb.local/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)"
10.0.2.18 - - [12/Mar/2026:09:21:11 +0100] "GET /api/fiches/1190 HTTP/1.1" 404 30 "https://frais.gsb.local/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)"
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"

On y lit trois histoires :

  • 198.51.100.23 parcourt des adresses classiques (/.env, /.git/config, /phpmyadmin/, /wp-login.php) : un scanner automatique qui cherche des fichiers oubliés. Tout répond 404 : rien n'a fuité. C'est le bruit de fond d'Internet.
  • 203.0.113.80 envoie une injection SQL (%27 est l'apostrophe encodée, suivie de UNION SELECT login,mdp_hash FROM utilisateur) puis un parcours de répertoire (..%2F est ../). Son User-Agent annonce l'outil sqlmap. L'API répond 400 : la validation des entrées a rejeté les deux requêtes.
  • 10.0.2.18, un poste interne, lit sa fiche 1187 puis essaie 1188, 1189, 1190 en une seconde chacune : une tentative d'IDOR par un utilisateur authentifié. Le contrôle d'accès fonctionne : l'API répond 404 sans révéler les fiches, comme la route corrigée du chapitre 3. Mais une série de 404 sur des identifiants qui se suivent est un comportement à signaler.

Analyser avec la ligne de commande

Sur un serveur Linux, quelques commandes enchaînées suffisent pour une première analyse. awk découpe chaque ligne en champs séparés par des espaces ($1 est l'adresse IP, $7 le chemin, $9 le code de statut).

bash
# Nombre de requêtes par adresse IP, de la plus active à la moins active
awk '{print $1}' access.log | sort | uniq -c | sort -rn
text
   4 198.51.100.23
   4 10.0.2.18
   2 203.0.113.80
   2 10.0.2.31
bash
# Répartition des codes de statut
awk '{print $9}' access.log | sort | uniq -c | sort -rn
text
   7 404
   3 200
   2 400
bash
# Qui reçoit des 404 sur des fiches (refus d'accès déguisés) ?
awk '$9 == 404 && $7 ~ /^\/api\/fiches\// {print $1}' access.log | sort | uniq -c
text
   3 10.0.2.18
bash
# Requêtes qui ressemblent à une injection SQL ou à un parcours de répertoire
grep -Ei "union|select|%27|\.\.%2f|\.\./" access.log | awk '{print $1, $7}'
text
203.0.113.80 /api/fiches?mois=202603%27%20UNION%20SELECT%20login,mdp_hash%20FROM%20utilisateur--%20
203.0.113.80 /api/justificatifs?fichier=..%2F..%2F..%2Fetc%2Fpasswd

Sous Windows, PowerShell offre l'équivalent avec Select-String et Group-Object (cours Scripting PowerShell et Bash). Le cours Administration Linux détaille grep, awk et les journaux du système.

Analyser les connexions avec SQL

Quand les connexions sont journalisées dans une table, SQL devient l'outil d'analyse. C'est un exercice classique de l'E7. Voici le contenu de la table journal (colonnes utiles) pour la nuit et la matinée du 12 mars 2026 :

iddate_heureloginipresultat
12026-03-12 02:10:04mdurand203.0.113.45echec
22026-03-12 02:10:31mdurand203.0.113.45echec
32026-03-12 02:11:02mdurand203.0.113.45echec
42026-03-12 02:11:40mdurand203.0.113.45echec
52026-03-12 02:12:15mdurand203.0.113.45echec
62026-03-12 02:12:52mdurand203.0.113.45echec
72026-03-12 02:13:30mdurand203.0.113.45succes
82026-03-12 03:00:05agarcia198.51.100.7echec
92026-03-12 03:00:09lmartin198.51.100.7echec
102026-03-12 03:00:14pbernard198.51.100.7echec
112026-03-12 03:00:18sroux198.51.100.7echec
122026-03-12 03:00:23cpetit198.51.100.7echec
132026-03-12 08:47:12lmartin10.0.2.31echec
142026-03-12 08:47:30lmartin10.0.2.31succes
152026-03-12 09:02:44agarcia10.0.2.18succes
162026-03-12 09:15:03cpetit10.0.2.40succes

Les adresses IP qui accumulent les échecs (seuil : 5) :

sql
SELECT ip, COUNT(*) AS nb_echecs
FROM journal
WHERE resultat = 'echec'
GROUP BY ip
HAVING COUNT(*) >= 5
ORDER BY nb_echecs DESC;
ipnb_echecs
203.0.113.456
198.51.100.75

La condition sur resultat va dans le WHERE (elle filtre des lignes), la condition sur le nombre d'échecs dans le HAVING (elle filtre des groupes). Confondre les deux est le piège classique (WHERE ou HAVING).

Distinguer la force brute du password spraying. Les deux adresses ont un nombre d'échecs comparable, mais pas le même comportement. On compte les comptes distincts visés :

sql
SELECT ip, COUNT(DISTINCT login) AS nb_comptes, COUNT(*) AS nb_echecs
FROM journal
WHERE resultat = 'echec'
GROUP BY ip
HAVING COUNT(DISTINCT login) >= 3;
ipnb_comptesnb_echecs
198.51.100.755

198.51.100.7 a essayé un mot de passe sur cinq comptes en vingt secondes : c'est du password spraying. 203.0.113.45 s'acharne sur un seul compte : c'est de la force brute.

Le plus grave : un succès après une série d'échecs.

sql
SELECT date_heure, login, ip
FROM journal
WHERE resultat = 'succes'
  AND ip IN (SELECT ip FROM journal WHERE resultat = 'echec' GROUP BY ip HAVING COUNT(*) >= 5);
date_heureloginip
2026-03-12 02:13:30mdurand203.0.113.45

Après six échecs, l'attaquant a trouvé le mot de passe de mdurand à 2 h 13. Le compte est compromis : il faut le bloquer, réinitialiser le mot de passe, vérifier ce qui a été fait avec ce compte depuis 2 h 13, et se demander pourquoi six essais ont été possibles sans blocage.

Les connexions réussies hors des horaires de travail (avant 7 h ou à partir de 20 h) :

sql
SELECT id, date_heure, login, ip
FROM journal
WHERE resultat = 'succes'
  AND (HOUR(date_heure) < 7 OR HOUR(date_heure) >= 20);
iddate_heureloginip
72026-03-12 02:13:30mdurand203.0.113.45

En production, ces requêtes portent sur une fenêtre glissante : WHERE date_heure >= NOW() - INTERVAL 15 MINUTE pour les échecs des quinze dernières minutes.

Du journal à l'alerte

Analyser à la main après coup ne suffit pas : il faut être prévenu pendant l'attaque. Les journaux de l'application, du serveur web, du pare-feu et de la base sont envoyés vers une plateforme de centralisation (pile ELK ou OpenSearch, Graylog, Grafana Loki) ou un SIEM (Security Information and Event Management, comme Wazuh), qui applique des règles et déclenche des alertes. Quelques règles typiques :

RègleSeuil indicatifRéaction
Échecs de connexion par adresse IPPlus de 10 en 15 minutesBlocage temporaire de l'adresse
Échecs sur un même comptePlus de 10 en 15 minutesDélai croissant, alerte, information de l'utilisateur
Un même mot de passe sur plusieurs comptes5 comptes en quelques minutes depuis une même sourceBlocage de la source, alerte
Succès après une série d'échecsDès la première occurrenceAlerte immédiate, vérification du compte
Refus d'accès (403 ou 404 sur la ressource d'un autre) en série pour un même utilisateurPlus de 20 en une minuteAlerte : tentative d'IDOR probable
Erreurs 500 en hausse brutaleDoublement sur 5 minutesAlerte : panne ou attaque en cours
ApplicationServeur webBase de donnéesCentralisationSIEM, Wazuh, ELKRègles et seuilsplus de 10 échecs par IPen 15 minutesun mot de passe, n comptessuccès après des échecsrefus d'accès en sérieAlerteÉquipe de supervisionqualifie l'incidentbloquer l'adresse, verrouiller le compte,réinitialiser le mot de passe
Journaliser ne suffit pas : des règles transforment les événements en alertes traitées par quelqu'un

Des outils comme fail2ban lisent les journaux en continu et bloquent automatiquement au pare-feu les adresses qui dépassent un seuil. Les seuils s'ajustent pour éviter deux écueils : trop d'alertes (on finit par ne plus les lire) et trop peu (on rate l'attaque). Le cours Supervision présente la surveillance d'un système d'information.

Mal gérer une condition exceptionnelle (A10)

La catégorie A10:2025, Mishandling of Exceptional Conditions, est nouvelle. Elle regroupe les failles qui apparaissent quand l'application rencontre une situation inhabituelle (une erreur, une ressource indisponible, une entrée inattendue) et que le code ne l'empêche pas, ne la détecte pas ou y réagit mal. L'OWASP y range notamment l'échec « ouvert » (CWE-636), l'exposition d'informations dans les messages d'erreur et les exceptions non gérées.

Échouer ouvert. Le cas le plus grave : un contrôle de sécurité qui, en cas d'erreur, laisse passer.

js
// VULNÉRABLE : en cas d'erreur, l'accès est accordé
async function peutValider(utilisateur, idFiche) {
  try {
    const [lignes] = await pool.execute('SELECT id_visiteur, etat FROM fiche_frais WHERE id = ?', [idFiche]);
    return utilisateur.role === 'comptable' && lignes[0].etat === 'CL' && lignes[0].id_visiteur !== utilisateur.id;
  } catch (erreur) {
    console.error(erreur);
    return true;          // « pour ne pas bloquer les comptables si la base répond mal »
  }
}

Il suffit de provoquer une erreur (une fiche inexistante, donc lignes[0] indéfini, ou une base surchargée) pour que la validation soit accordée. La règle : en cas de doute, refuser (fail closed). Le catch renvoie false, journalise l'erreur, et la requête reçoit une réponse d'erreur.

Laisser tomber le serveur. Une exception non attrapée ou une promesse rejetée sans gestionnaire arrête un processus Node.js. Si une saisie particulière suffit à la provoquer, un attaquant la répète et rend l'API indisponible : c'est un déni de service.

Laisser un état incohérent. Une opération en plusieurs étapes (débiter, puis créditer ; changer l'état de la fiche, puis déclencher le virement) interrompue au milieu laisse des données fausses. L'OWASP recommande d'annuler complètement l'opération plutôt que de tenter de récupérer l'état partiel : c'est le rôle des transactions (COMMIT et ROLLBACK).

Bloquer des ressources. Un fichier, une connexion à la base ou un verrou ouverts avant une erreur et jamais libérés finissent par épuiser les ressources du serveur.

Révéler des informations. Une erreur de base de données affichée telle quelle donne la structure des tables, ce qui aide à préparer une injection (chapitre 4).

Gérer les erreurs proprement

Les recommandations de l'OWASP pour A10, appliquées à AppliFrais :

  1. Attraper l'erreur là où elle se produit, et la traiter de façon utile : message compréhensible pour l'utilisateur, détail dans le journal, alerte si nécessaire.
  2. Un gestionnaire d'erreurs global comme filet de sécurité pour tout ce qui aurait échappé.
  3. Échouer en refusant, et annuler complètement une transaction interrompue.
  4. Fixer des limites à tout ce qui consomme des ressources : taille des requêtes, taille des fichiers, durée des appels, nombre de requêtes par client. Aucune ressource ne doit être illimitée.
  5. Centraliser la gestion des erreurs, pour qu'elle soit la même partout.

Avec Express 5, une erreur levée dans une route asynchrone est transmise automatiquement au gestionnaire d'erreurs (avec Express 4, il fallait l'attraper et appeler next(erreur)). Le gestionnaire global se déclare en dernier, avec quatre paramètres :

js
import crypto from 'node:crypto';

app.use(express.json({ limit: '100kb' }));     // limite la taille des corps JSON

// … les routes …

// Gestionnaire d'erreurs global : toujours en dernier
app.use((erreur, req, res, next) => {
  const reference = crypto.randomUUID();
  journal.error({ reference, route: `${req.method} ${req.originalUrl}`, erreur: erreur.message, pile: erreur.stack });
  res.status(erreur.status ?? 500).json({
    erreur: 'Une erreur est survenue',
    reference,               // l'utilisateur peut la communiquer au support, qui retrouve le détail
  });
});

L'utilisateur reçoit un message générique et une référence ; le développeur retrouve tout le détail dans le journal grâce à cette référence. Pour une opération en plusieurs étapes, la transaction garantit le « tout ou rien » :

js
const connexion = await pool.getConnection();
try {
  await connexion.beginTransaction();
  await connexion.execute("UPDATE fiche_frais SET etat = 'RB' WHERE id = ? AND etat = 'VA'", [idFiche]);
  await connexion.execute('INSERT INTO paiement (id_fiche, montant, date_ordre) VALUES (?, ?, NOW())', [idFiche, montant]);
  await connexion.commit();
} catch (erreur) {
  await connexion.rollback();     // rien n'est enregistré : pas d'état à moitié fait
  throw erreur;                   // le gestionnaire global journalise et répond
} finally {
  connexion.release();            // la connexion est rendue au pool dans tous les cas
}

En PHP, en production : display_errors = Off, log_errors = On, un gestionnaire global avec set_exception_handler(), et PDO en mode exception (PDO::ERRMODE_EXCEPTION) pour qu'aucune erreur de base ne passe inaperçue.

Exercice 1 sur 4 Requêtes sur le journal Réussi À revoir

On utilise la table journal du chapitre (16 lignes du 12 mars 2026). Écris chaque requête et donne son résultat.

  1. Le nombre d'échecs de connexion par login, du plus grand au plus petit (à égalité, par ordre alphabétique).
  2. Les adresses IP qui ont échoué au moins une fois, avec leur nombre d'échecs.
  3. Le nombre total de connexions réussies et le nombre total d'échecs.
  4. Les connexions réussies depuis une adresse IP externe (qui ne commence pas par 10.).
Voir la réponseMasquer la réponse de l'exercice 1

1.

sql
SELECT login, COUNT(*) AS nb_echecs
FROM journal
WHERE resultat = 'echec'
GROUP BY login
ORDER BY nb_echecs DESC, login;
loginnb_echecs
mdurand6
lmartin2
agarcia1
cpetit1
pbernard1
sroux1

lmartin compte 2 échecs : un pendant le password spraying (ligne 9) et une faute de frappe à 8 h 47 (ligne 13).

sql
SELECT ip, COUNT(*) AS nb_echecs
FROM journal
WHERE resultat = 'echec'
GROUP BY ip;
ipnb_echecs
10.0.2.311
198.51.100.75
203.0.113.456

(L'ordre des lignes n'est pas garanti sans ORDER BY.)

sql
SELECT resultat, COUNT(*) AS nb
FROM journal
GROUP BY resultat;
resultatnb
echec12
succes4
sql
SELECT id, date_heure, login, ip
FROM journal
WHERE resultat = 'succes' AND ip NOT LIKE '10.%';
iddate_heureloginip
72026-03-12 02:13:30mdurand203.0.113.45

Une seule connexion réussie vient de l'extérieur, et c'est celle de l'attaquant.

Tu avais trouvé ?

Exercice 2 sur 4 Interpréter le journal Réussi À revoir

Toujours avec la table journal du chapitre :

  1. Que s'est-il passé entre 2 h 10 et 2 h 14 ? Quel critère DICP est menacé ?
  2. Que s'est-il passé à 3 h 00 ? Pourquoi un blocage par compte après 5 échecs ne l'aurait-il pas arrêté ?
  3. Propose trois mesures, en précisant pour chacune si elle relève de la prévention ou de la détection.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Une attaque par force brute depuis 203.0.113.45 sur le compte mdurand : six échecs en moins de trois minutes, puis un succès à 2 h 13 min 30 s. Le compte est compromis : la confidentialité et l'intégrité des données accessibles avec ce compte sont menacées, et la preuve aussi (les actions de l'attaquant seront attribuées à mdurand).
  2. Un password spraying depuis 198.51.100.7 : un essai sur chacun de cinq comptes différents en vingt secondes. Chaque compte n'a subi qu'un échec : un blocage par compte, déclenché à 5 échecs, ne voit rien. Il faut une limite par adresse IP et une règle qui compte les comptes distincts visés par une même source.
  3. Par exemple : limitation des tentatives par compte et par adresse IP, avec délai croissant (prévention) ; MFA sur tous les comptes, qui aurait rendu le mot de passe trouvé inutile (prévention) ; alerte en temps réel sur « succès après une série d'échecs » et sur « plusieurs comptes visés par une même adresse » (détection). Il faut aussi traiter l'incident : bloquer mdurand, réinitialiser son mot de passe, examiner ses actions depuis 2 h 13.

Tu avais trouvé ?

Exercice 3 sur 4 Fail open Réussi À revoir

Voici un middleware de vérification des droits :

js
async function verifierDroit(req, res, next) {
  try {
    const autorise = await serviceDroits.verifier(req.utilisateur.id, req.path);
    if (!autorise) return res.status(403).end();
  } catch (e) {
    console.log('service des droits indisponible');
  }
  next();
}
  1. Que se passe-t-il quand le service des droits est indisponible ?
  2. Dans quelle catégorie du Top 10 2025 classes-tu ce défaut ?
  3. Corrige le middleware.
Voir la réponseMasquer la réponse de l'exercice 3
  1. L'exception est attrapée, un message est écrit, puis next() est appelé : toutes les requêtes passent, sans contrôle. Un attaquant qui parvient à saturer ou à faire tomber le service des droits obtient un accès total.
  2. A10:2025 Mauvaise gestion des conditions exceptionnelles (échec ouvert, CWE-636). La conséquence est un contrôle d'accès contourné (A01).
  3. On refuse en cas d'erreur et on journalise correctement :
js
async function verifierDroit(req, res, next) {
  try {
    const autorise = await serviceDroits.verifier(req.utilisateur.id, req.path);
    if (!autorise) return res.status(403).end();
    next();
  } catch (e) {
    journal.error({ evenement: 'service_droits_indisponible', erreur: e.message });
    return res.status(503).json({ erreur: 'Service momentanément indisponible' });
  }
}

Tu avais trouvé ?

Exercice 4 sur 4 Que journaliser ? Réussi À revoir

Pour chaque proposition, dis si on la journalise, et sinon pourquoi.

  1. Le login et l'adresse IP de chaque échec de connexion.
  2. Le mot de passe saisi lors d'un échec, « pour comprendre les erreurs des utilisateurs ».
  3. La validation d'une fiche : identifiant du comptable, fiche, date et heure.
  4. Le jeton JWT complet de chaque requête.
  5. Les refus d'accès (403) avec l'utilisateur et la ressource demandée.
Voir la réponseMasquer la réponse de l'exercice 4
  1. Oui : indispensable pour détecter la force brute et le password spraying.
  2. Non, jamais : un mot de passe erroné est souvent le vrai à une faute près, ou le mot de passe d'un autre service. Le journal deviendrait une liste de mots de passe.
  3. Oui : c'est une action sensible ; la trace assure la preuve.
  4. Non : quiconque lit le journal pourrait réutiliser un jeton encore valide. On journalise l'identifiant de l'utilisateur extrait du jeton.
  5. Oui : une série de refus révèle une tentative d'IDOR ou d'élévation de privilèges.

Tu avais trouvé ?

Pour aller plus loin. Le cours Journalisation et réponse à incident reprend la lecture des logs côté serveur (Lire et analyser des logs), la centralisation dans un SIEM et la conduite d'un incident de bout en bout.

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Sécuriser une API REST et une application mobileRevoir le sommaire du cours