First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/.idea
|
||||||
198
README.md
Normal file
198
README.md
Normal 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
51
app.js
Normal 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
90
bin/www
Normal 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
1907
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal 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
109
routes/index.js
Normal 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;
|
||||||
Reference in New Issue
Block a user