First commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/.idea
|
||||
node_modules
|
||||
132
README.md
Normal file
132
README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# TP Fix'it - audit & sécurisation
|
||||
|
||||
## Contexte de la mission
|
||||
|
||||
Vous venez de rejoindre l'équipe technique d'une jeune startup, **"SafeNote"**.
|
||||
Le développeur précédent, pressé par les délais, a quitté l'entreprise précipitamment la semaine dernière. Il a laissé derrière lui un prototype fonctionnel (le fichier `server.js`) qui permet aux utilisateurs de s'inscrire et de gérer des notes privées.
|
||||
|
||||
**Le problème ?**
|
||||
Le code est fonctionnel, mais **catastrophique** en termes de sécurité. Votre CTO a identifié plusieurs failles critiques du **Top 10 OWASP** et vous charge de nettoyer ce code avant la mise en production.
|
||||
|
||||
---
|
||||
|
||||
## Installation & Démarrage
|
||||
|
||||
Assurez-vous d'avoir lu le fichier `server.js` (fourni) à la racine de votre projet.
|
||||
|
||||
1. **Initialiser le projet et installer les dépendances :**
|
||||
Dans votre terminal, exécutez les commandes suivantes :
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. Lancer le serveur :
|
||||
```bash
|
||||
nodemon server.js
|
||||
```
|
||||
3. Tester l'application actuelle : L'API écoute sur le port 3000. Vous pouvez utiliser Postman à cet effet.
|
||||
- POST `/login` (Body JSON : `{"username": "admin", "password": "supersecret"}`)
|
||||
- POST `/notes` (Body JSON : `{"userId": 1, "content": "Ma note"}`)
|
||||
- GET `/notes/1`
|
||||
|
||||
---
|
||||
|
||||
## Objectifs de la mission
|
||||
|
||||
Votre mission est d'identifier et de corriger **4 failles majeures**.
|
||||
|
||||
### 1. Stopper l'hémorragie SQL (OWASP A05:2025 Injection)
|
||||
Actuellement, la connexion est vulnérable. Un attaquant peut se connecter sans mot de passe en utilisant une injection SQL simple dans le champ `username` (ex: `admin' --`).
|
||||
* **Tâche :** remplacez la concaténation de chaînes par des **requêtes préparées** (Prepared Statements) de SQLite.
|
||||
|
||||
**Flux de l'attaque (actuel) :**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Input utilisateur: 'admin' --'"] --> B["Serveur concatène la requête: '...WHERE user = '' + 'admin' --' + ''...'"];
|
||||
B --> C["Requête SQL exécutée: '...WHERE user = 'admin' -- ...'"];
|
||||
C --> D{Base de données};
|
||||
D -- "Commentaire SQL '--' ignore le mdp" --> E[Accès accordé];
|
||||
```
|
||||
|
||||
**Flux sécurisé (cible) :**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Input utilisateur: 'admin' --'"] --> B["Serveur utilise une requête préparée: '...WHERE user = ?'"];
|
||||
B --> C["Serveur envoie la donnée 'admin' --' séparément"];
|
||||
C --> D{Base de données};
|
||||
D -- "La donnée est traitée comme du texte, pas du code" --> E[Accès refusé];
|
||||
```
|
||||
|
||||
### 2. Protéger les mots de Passe (OWASP A04:2025 Cryptographic Failures)
|
||||
Les mots de passe sont stockés en clair dans la base de données (`supersecret`, `password123`). C'est inacceptable.
|
||||
* **Tâche :** modifiez le code pour hacher les mots de passe avec **bcrypt** lors de leur création (simulation) et vérifier le hash lors du login.
|
||||
|
||||
**Flux de vérification sécurisé (cible) :**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Inscription
|
||||
P1["Mdp utilisateur: 'password123'"] --> P2["bcrypt.hash 'password123'"];
|
||||
P2 --> P3["Stocke le hash '$2b$10$...' en BDD"];
|
||||
end
|
||||
subgraph Connexion
|
||||
A["Input: 'password123'"] --> B["Récupère le hash '$2b$10$...' de la BDD"];
|
||||
B --> C["Compare l'input au hash via 'bcrypt.compare'"];
|
||||
C --> D{Match?};
|
||||
D -- "Oui" --> E[Accès accordé];
|
||||
D -- "Non" --> F[Accès refusé];
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Cloisonner les données (OWASP A01:2025 Broken Access Control)
|
||||
Actuellement, un utilisateur peut lire les notes de n'importe qui en changeant simplement l'ID dans l'URL (ex: passer de `/notes/1` à `/notes/2`). C'est une faille **IDOR** (Insecure Direct Object Reference).
|
||||
* **Tâche :** assurez-vous que l'utilisateur qui demande la note est bien le **propriétaire** de cette note.
|
||||
* *Indice :* vous devrez probablement simuler ou implémenter une vérification de l'utilisateur courant (via un middleware ou une vérification d'ID).
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Requête: GET /notes/5"] -- "Session: utilisateurId = 1" --> B["Serveur récupère la note 5"];
|
||||
B --> C{Note 5 existe?};
|
||||
C -- "Non" --> D[Réponse: 404 Not Found];
|
||||
C -- "Oui" --> E["Récupère le propriétaire: 'note.proprietaireId = 2'"];
|
||||
E --> F{"Le 'proprietaireId' 2 correspond à 'l'utilisateurId' 1?"};
|
||||
F -- "Non (IDOR bloqué)" --> G[Réponse: 403 Forbidden / 404 Not Found];
|
||||
F -- "Oui" --> H[Réponse: Affiche la note 5];
|
||||
```
|
||||
|
||||
### 4. Nettoyer les sorties (OWASP A05:2025 Injection / XSS)
|
||||
Si une note contient du code HTML/JS (ex: `<script>alert('Hacked')</script>`), l'application l'exécute tel quel dans le navigateur.
|
||||
* **Tâche :** mettez en place un **encodage de sortie** (Output Encoding) ou utilisez une bibliothèque d'échappement pour neutraliser le HTML avant de le renvoyer au client.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Input: '<script>alert'"] --> B["Stocké en BDD tel quel"];
|
||||
B --> C["Requête GET pour la note"];
|
||||
C --> D["Serveur envoie la donnée brute au navigateur"];
|
||||
D --> E["Navigateur interprète le HTML: exécution du script"];
|
||||
```
|
||||
|
||||
**Flux sécurisé (cible) :**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Input: '<script>alert'"] --> B["Stocké en BDD tel quel"];
|
||||
B --> C["Requête GET pour la note"];
|
||||
C --> D["Serveur 'échappe' les caractères: '<script>...'"];
|
||||
D --> E["Navigateur affiche le texte 'échappé' sans l'exécuter"];
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Bonus (pour aller plus loin)
|
||||
|
||||
Si vous avez terminé les corrections principales :
|
||||
|
||||
* **Security Headers (OWASP A02:2025 - Security Misconfiguration)** : utilisez le middleware `helmet` pour ajouter automatiquement les en-têtes HTTP de sécurité.
|
||||
* **Authentification robuste** : remplacez l'envoi brut de l'`userId` par un véritable mécanisme de token **JWT** (JSON Web Token).
|
||||
|
||||
---
|
||||
|
||||
Bon courage !
|
||||
3359
package-lock.json
generated
Normal file
3359
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "fix-it",
|
||||
"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",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "~4.16.1",
|
||||
"helmet": "^8.1.0",
|
||||
"http-errors": "~1.6.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "~1.9.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"pug": "2.0.0-beta11",
|
||||
"sqlite3": "^5.1.7"
|
||||
}
|
||||
}
|
||||
62
server.js
Normal file
62
server.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const express = require('express');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const bcrypt = require('bcrypt');
|
||||
const helmet = require('helmet');
|
||||
const escape = require('escape-html');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// BDD simulée en mémoire
|
||||
const db = new sqlite3.Database(':memory:');
|
||||
db.serialize(() => {
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)");
|
||||
db.run("CREATE TABLE notes (id INTEGER PRIMARY KEY, user_id INTEGER, content TEXT)");
|
||||
// Mot de passe stocké en clair (Faille 1)
|
||||
db.run("INSERT INTO users (username, password) VALUES ('admin', 'supersecret')");
|
||||
db.run("INSERT INTO users (username, password) VALUES ('alice', 'password123')");
|
||||
db.run("INSERT INTO notes (user_id, content) VALUES (2, 'Mon secret personnel')");
|
||||
});
|
||||
|
||||
// Route de connexion
|
||||
app.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
// Faille 2 : Injection SQL (Concaténation directe)
|
||||
const query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
|
||||
|
||||
db.get(query, (err, row) => {
|
||||
if (err) return res.status(500).send(err.message);
|
||||
if (row) {
|
||||
res.json({ message: "Connecté", userId: row.id });
|
||||
} else {
|
||||
res.status(401).send("Échec");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Route pour lire une note
|
||||
app.get('/notes/:id', (req, res) => {
|
||||
// Faille 3 : Broken Access Control (IDOR)
|
||||
// On ne vérifie pas si la note appartient à l'utilisateur qui la demande
|
||||
const query = `SELECT * FROM notes WHERE id = ${req.params.id}`;
|
||||
db.get(query, (err, row) => {
|
||||
if (row) {
|
||||
// Faille 4 : XSS Réfléchi/Stocké (Pas d'échappement de la sortie)
|
||||
res.send(`<h1>Note : ${row.content}</h1>`);
|
||||
} else {
|
||||
res.status(404).send("Note introuvable");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Route pour créer une note (Vulnérable XSS à l'insertion)
|
||||
app.post('/notes', (req, res) => {
|
||||
const { userId, content } = req.body;
|
||||
const query = `INSERT INTO notes (user_id, content) VALUES (${userId}, '${content}')`;
|
||||
db.run(query, function(err) {
|
||||
if (err) return res.status(500).send(err.message);
|
||||
res.send("Note créée");
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(3000, () => console.log('Server running on port 3000'));
|
||||
Reference in New Issue
Block a user