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