First commit

This commit is contained in:
Johan
2025-12-18 15:21:19 +01:00
commit 559a61c33e
7 changed files with 2376 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/.idea

198
README.md Normal file
View File

@@ -0,0 +1,198 @@
# Démonstration A07:2025 - Authentication Failures
## Objectif de la démonstration
Cet atelier illustre concrètement plusieurs vulnérabilités de la catégorie **A07:2025 - Authentication Failures** (échecs d'authentification) de l'owasp.
Vous n'avez **aucun code à écrire**. Votre rôle est d'analyser le comportement de deux points de terminaison (endpoints) à l'aide de postman et d'observer les journaux (logs) du serveur.
## Contexte du projet
Le code `routes/index.js` contient deux routes d'authentification:
1. `POST /login-vulnerable`: une implémentation intentionnellement faible.
2. `POST /login-securise`: une implémentation corrigée et sécurisée.
## Prérequis
* Node.js et npm installés.
* Le logiciel postman (ou un équivalent pour tester les api).
* Ce projet (avec les `node_modules` installés via `npm install`).
## Instructions de lancement
1. Ouvrez un terminal (par exemple, le terminal intégré de webstorm).
2. Lancez le serveur en mode "watch" avec la commande:
```bash
nodemon ./bin/www
```
3. Gardez cette console visible. C'est ici que vous lirez les journaux (logs) du serveur.
4. Ouvrez postman pour effectuer les tests décrits ci-dessous.
---
## Partie 1: Analyse du point de terminaison vulnérable
Testez l'endpoint `POST http://localhost:3000/login-vulnerable` avec postman.
Assurez-vous d'envoyer vos données en `raw` -> `json` dans l'onglet `Body`.
```mermaid
flowchart TD
a["client envoie requête vers /login-vulnerable"] --> b["serveur recherche l'utilisateur"]
b --> c{"utilisateur trouvé ?"}
c -- "non" --> d["retour 404 avec message 'utilisateur non trouvé'"]
c -- "oui" --> e{"mot de passe valable ?"}
e -- "non" --> f["retour 400 avec message 'mot de passe incorrect'"]
e -- "oui" --> g["session non régénérée"]
g --> h["retour 200 'connexion vulnérable réussie'"]
```
### Faille 1: énumération de comptes
L'énumération de comptes permet à un attaquant de deviner quels utilisateurs existent dans la base de données en analysant les différentes réponses du serveur.
**Test A: utilisateur inexistant**
* Requête (body):
```json
{
"username": "un_utilisateur_inconnu",
"password": "123"
}
```
* Réponse (status 404): `Erreur : Utilisateur non trouvé.`
**Test B: utilisateur existant, mauvais mot de passe**
* Requête (body):
```json
{
"username": "admin",
"password": "mauvais_mot_de_passe"
}
```
* Réponse (status 400): `Erreur : Mot de passe incorrect.`
**Conclusion de la faille 1:**
Les messages d'erreur sont différents. Un attaquant peut créer un script pour tester des milliers de noms d'utilisateurs et savoir lesquels sont valides (ceux qui retournent "mot de passe incorrect").
### Faille 2: mots de passe en clair et absence de limitation
Cette route compare directement le mot de passe reçu avec un mot de passe stocké en clair (`password_vulnerable`).
```mermaid
flowchart TD
a["client envoie requête vers /login-securise"] --> b["serveur recherche l'utilisateur (sans distinction dans la réponse)"]
b --> c["vérification avec 'bcrypt.compare'"]
c --> d{"identifiants valides ?"}
d -- "non" --> e["retour 401 'identifiants invalides'"]
d -- "oui" --> f["regeneration de la session"]
f --> g["retour 200 'connexion sécurisée réussie'"]
```
**Test C: attaque par force brute**
* Le serveur n'implémente aucune limitation de tentatives (rate limiting).
* Un attaquant pourrait tester des millions de mots de passe pour l'utilisateur "admin" sans jamais être bloqué.
### Faille 3: fixation de session
La fixation de session se produit lorsque l'identifiant de session d'un utilisateur n'est pas renouvelé après une authentification réussie.
**Test D: connexion réussie**
* Requête (body):
```json
{
"username": "admin",
"password": "password123"
}
```
* Réponse (status 200): `Connexion (vulnérable) réussie pour admin !`
* **Action requise:** regardez maintenant la console de votre serveur (dans webstorm).
* Vous devriez voir les logs suivants:
```
[VULNÉRABLE] Session AVANT login: [un long identifiant]
[VULNÉRABLE] Session APRÈS login: [le même identifiant] (INCHANGÉE)
```
**Conclusion de la faille 3:**
L'identifiant de session est le même avant et après le login. Si un attaquant parvenait à "fixer" (donner) un identifiant de session à un utilisateur avant sa connexion (par exemple, via un lien piégé), il pourrait usurper son identité une fois l'utilisateur connecté.
---
## Partie 2: Analyse du point de terminaison corrigé
Testez maintenant l'endpoint `POST http://localhost:3000/login-securise`.
### Correction 1: prévention de l'énumération de comptes
**Test A (corrigé): utilisateur inexistant**
* Requête (body):
```json
{
"username": "un_utilisateur_inconnu",
"password": "123"
}
```
* Réponse (status 401): `Identifiants invalides.`
**Test B (corrigé): utilisateur existant, mauvais mot de passe**
* Requête (body):
```json
{
"username": "admin",
"password": "mauvais_mot_de_passe"
}
```
* Réponse (status 401): `Identifiants invalides.`
**Conclusion de la correction 1:**
La réponse est identique dans les deux cas. Il est désormais impossible pour un attaquant de distinguer un nom d'utilisateur invalide d'un mot de passe invalide.
### Correction 2: hachage et régénération de session
**Test D (corrigé): connexion réussie**
* Le code utilise `bcrypt.compare` pour comparer de manière sécurisée le mot de passe fourni avec le hash stocké.
* Requête (body):
```json
{
"username": "admin",
"password": "password123"
}
```
* Réponse (status 200): `Connexion (sécurisée) réussie pour admin !`
* **Action requise:** regardez à nouveau la console du serveur.
* Vous devriez voir ces logs:
```
[SÉCURISÉ] Session AVANT login: [un premier identifiant]
[SÉCURISÉ] Session APRÈS login: [un nouvel identifiant] (CHANGÉE)
```
**Conclusion de la correction 2:**
Le serveur a explicitement régénéré la session (`req.session.regenerate`). L'ancien identifiant est invalidé et un nouveau est créé, empêchant toute attaque de type "fixation de session".
### Correction 3: limitation des tentatives (rate limiting)
**Test E (corrigé): attaque par force brute**
* Envoyez la requête du "Test B (corrigé)" (avec un mauvais mot de passe) 11 fois de suite.
* Les 10 premières tentatives renverront `Identifiants invalides.`
* À la 11ème tentative (et pour les suivantes pendant 15 minutes), vous recevrez:
* Réponse (status 429): `Trop de tentatives de connexion. Réessayez dans 15 minutes.`
**Conclusion de la correction 3:**
Le serveur empêche activement les attaques par force brute ou par "credential stuffing" en limitant le nombre d'échecs autorisés par adresse ip.
## Conclusion
Cette démonstration a mis en évidence plusieurs vulnérabilités courantes liées à l'authentification, ainsi que les mesures correctives appropriées pour les atténuer.
```mermaid
flowchart LR
a["vulnérabilité : énumération de comptes"] --> b["risque : découverte des utilisateurs valides"]
c["vulnérabilité : mots de passe en clair"] --> d["risque : compromission immédiate"]
e["vulnérabilité : pas de rate limiting"] --> f["risque : force brute illimitée"]
g["vulnérabilité : session non régénérée"] --> h["risque : fixation de session"]
i["mesure : message d'erreur uniforme"] --> a
j["mesure : mot de passe hashé"] --> c
k["mesure : rate limiting"] --> e
l["mesure : regeneration de session"] --> g
```

51
app.js Normal file
View File

@@ -0,0 +1,51 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var indexRouter = require('./routes/index');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Activation du middleware de session
// DOIT être placé AVANT l'utilisation du routeur (app.use('/', indexRouter))
app.use(session({
// !! IMPORTANT !! Changez ce secret pour une longue phrase aléatoire en production
secret: 'un-secret-temporaire-pour-le-developpement',
resave: false,
saveUninitialized: true,
cookie: {
secure: false // Mettez 'true' si votre site est en HTTPS
}
}));
app.use('/', indexRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

90
bin/www Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('authentification:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

1907
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "authentification",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"bcrypt": "^6.0.0",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-rate-limit": "^8.2.1",
"express-session": "^1.18.2",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"nodemon": "^3.1.10",
"pug": "2.0.0-beta11"
}
}

109
routes/index.js Normal file
View File

@@ -0,0 +1,109 @@
var express = require('express');
var router = express.Router();
var bcrypt = require('bcrypt');
var rateLimit = require('express-rate-limit');
// --- Base de données simulée ---
const db = {
users: [
{
id: 1,
username: 'admin',
password_vulnerable: 'password123',
hashedPassword: bcrypt.hashSync('password123', 12)
}
]
};
/* GET home page */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
// ==========================================================
// ## 👎 DÉMONSTRATION VULNÉRABLE (A07)
// ==========================================================
router.post('/login-vulnerable', (req, res) => {
// !! REPOSE SUR express.json() et express.session() définis dans app.js !!
const { username, password } = req.body;
const user = db.users.find(u => u.username === username);
// FAILLE 1 : Énumération de comptes
if (!user) {
return res.status(404).send('Erreur : Utilisateur non trouvé.');
}
// FAILLE 2 : Stockage et comparaison en clair
if (user.password_vulnerable !== password) {
return res.status(400).send('Erreur : Mot de passe incorrect.');
}
// FAILLE 3 : Fixation de session
req.session.userId = user.id;
req.session.username = user.username;
console.log(`[VULNÉRABLE] Session AVANT login: ${req.sessionID}`);
console.log(`[VULNÉRABLE] Session APRÈS login: ${req.sessionID} (INCHANGÉE)`);
res.send(`Connexion (vulnérable) réussie pour ${user.username} !`);
});
// ==========================================================
// ## DÉMONSTRATION CORRIGÉE (A07)
// ==========================================================
// CORRECTION 4 : Limitation de tentatives (Rate Limiting)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Limite à 10 tentatives par IP par fenêtre de 15 min
message: 'Trop de tentatives de connexion. Réessayez dans 15 minutes.'
});
// Applique le limiteur uniquement à ce point de terminaison
router.use('/login-securise', loginLimiter);
router.post('/login-securise', async (req, res) => {
// !! REPOSE SUR express.json() et express.session() définis dans app.js !!
const { username, password } = req.body;
const user = db.users.find(u => u.username === username);
// CORRECTION 1 & 2 : Pas d'énumération + Hachage fort
const match = user ? await bcrypt.compare(password, user.hashedPassword) : false;
if (!match) {
return res.status(401).send('Identifiants invalides.');
}
// CORRECTION 3 : Régénération de la session
const oldSessionId = req.sessionID;
req.session.regenerate((err) => {
if (err) {
console.error(err);
return res.status(500).send('Erreur lors de la régénération de session.');
}
req.session.userId = user.id;
req.session.username = user.username;
console.log(`[SÉCURISÉ] Session AVANT login: ${oldSessionId}`);
console.log(`[SÉCURISÉ] Session APRÈS login: ${req.sessionID} (CHANGÉE)`);
res.send(`Connexion (sécurisée) réussie pour ${user.username} !`);
});
});
// --- Point de déconnexion ---
router.get('/logout', (req, res) => {
// !! REPOSE SUR express.session() défini dans app.js !!
req.session.destroy((err) => {
if (err) {
return res.status(500).send('Impossible de se déconnecter.');
}
res.clearCookie('connect.sid'); // Nom par défaut du cookie de session
res.send('Déconnecté avec succès.');
});
});
module.exports = router;