OWASP Top 10¶
Les dix vulnérabilités les plus critiques des applications web — comprendre, détecter et corriger avec des exemples concrets.
Vue d'ensemble OWASP Top 10 (2021)¶
graph TD
A01["A01 Broken Access Control"] --> X[Impact critique]
A02["A02 Cryptographic Failures"] --> X
A03["A03 Injection"] --> X
A04["A04 Insecure Design"] --> Y[Impact eleve]
A05["A05 Security Misconfiguration"] --> Y
A06["A06 Vulnerable Components"] --> Y
A07["A07 XSS"] --> Z[Impact moyen/eleve]
A08["A08 Integrity Failures"] --> Z
A09["A09 Logging Failures"] --> W[Impact moyen]
A10["A10 SSRF"] --> X
style X fill:#c0392b,color:#fff
style Y fill:#e07b39,color:#fff
style Z fill:#e6a817,color:#fff
style W fill:#7ab648,color:#fff | Rang | Vulnérabilité | Fréquence | Impact |
|---|---|---|---|
| A01 | Broken Access Control | Très haute | Critique |
| A02 | Cryptographic Failures | Haute | Critique |
| A03 | Injection | Haute | Critique |
| A04 | Insecure Design | Moyenne | Élevé |
| A05 | Security Misconfiguration | Très haute | Élevé |
| A06 | Vulnerable & Outdated Components | Haute | Élevé |
| A07 | XSS | Haute | Moyen |
| A08 | Software & Data Integrity Failures | Moyenne | Élevé |
| A09 | Logging & Monitoring Failures | Haute | Moyen |
| A10 | SSRF | Croissante | Critique |
A01 — Broken Access Control¶
Le contrôle d'accès defaillant est la vulnérabilité numéro 1. Un utilisateur peut accéder à des ressources ou actions auxquelles il ne devrait pas avoir accès.
IDOR (Insecure Direct Object Référence)¶
# VULNERABLE — l'utilisateur controle l'ID directement
@app.route("/api/orders/<int:order_id>")
def get_order(order_id):
order = db.query("SELECT * FROM orders WHERE id = ?", order_id)
return jsonify(order) # N'importe qui peut lire n'importe quelle commande
# SECURISE — verifier que la ressource appartient a l'utilisateur
@app.route("/api/orders/<int:order_id>")
@login_required
def get_order(order_id):
order = db.query(
"SELECT * FROM orders WHERE id = ? AND user_id = ?",
order_id, current_user.id
)
if not order:
abort(404)
return jsonify(order)
Erreur courante
Retourner une 403 au lieu d'une 404 révélé l'existence de la ressource. Toujours retourner 404 quand la ressource n'appartient pas à l'utilisateur.
A02 — Cryptographic Failures¶
Utilisation d'algorithmes deprecies, stockage en clair, transmissions non chiffrees.
Hachage de mots de passe¶
import hashlib
import bcrypt
# VULNERABLE — MD5 cassable en secondes
def store_password_bad(password):
return hashlib.md5(password.encode()).hexdigest()
# VULNERABLE — SHA256 sans sel, vulnerable aux rainbow tables
def store_password_bad2(password):
return hashlib.sha256(password.encode()).hexdigest()
# SECURISE — bcrypt avec facteur de cout adapte
def store_password(password):
salt = bcrypt.gensalt(rounds=12)
return bcrypt.hashpw(password.encode(), salt)
def verify_password(password, hashed):
return bcrypt.checkpw(password.encode(), hashed)
Chiffrement de données sensibles¶
from cryptography.fernet import Fernet
# VULNERABLE — AES-ECB, patterns identiques produisent des blocs identiques
# Ne jamais utiliser ECB pour des donnees structurees
# SECURISE — Fernet (AES-128-CBC + HMAC-SHA256)
key = Fernet.generate_key() # stocker en secrets manager, pas dans le code
fernet = Fernet(key)
def encrypt_pii(data: str) -> bytes:
return fernet.encrypt(data.encode())
def decrypt_pii(token: bytes) -> str:
return fernet.decrypt(token).decode()
A03 — Injection¶
Toute donnée non fiable interprétée comme commande ou requête. SQL, OS, LDAP, XPath, NoSQL...
Injection SQL¶
# VULNERABLE — concatenation directe
def get_user_bad(username):
query = f"SELECT * FROM users WHERE username = '{username}'"
return db.execute(query).fetchone()
# username = "admin' OR '1'='1" => tous les utilisateurs
# SECURISE — requetes parametrees
def get_user(username):
query = "SELECT * FROM users WHERE username = ?"
return db.execute(query, (username,)).fetchone()
Injection de commande OS¶
import subprocess
import shlex
# VULNERABLE — shell=True avec entree utilisateur
def ping_host_bad(host):
result = subprocess.run(f"ping -c 1 {host}", shell=True, capture_output=True)
return result.stdout
# host = "8.8.8.8; cat /etc/passwd" => execution arbitraire
# SECURISE — liste d'arguments, validation stricte
import re
def ping_host(host):
if not re.match(r'^[a-zA-Z0-9.\-]{1,253}$', host):
raise ValueError("Hostname invalide")
result = subprocess.run(["ping", "-c", "1", host], capture_output=True, timeout=5)
return result.stdout
Injection NoSQL
MongoDB est aussi vulnerable. Ne jamais passer un objet utilisateur directement :
// VULNERABLE
db.users.find({ username: req.body.username, password: req.body.password })
// req.body = { username: "admin", password: { $gt: "" } } => bypass
// SECURISE — valider que ce sont des strings
const { username, password } = req.body;
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Input invalide' });
}
A07 — Cross-Site Scripting (XSS)¶
Injection de code JavaScript dans des pages affichees par d'autres utilisateurs.
XSS Reflechi¶
// VULNERABLE — rendu direct de l'input dans le DOM
// URL: /search?q=<script>document.cookie</script>
app.get('/search', (req, res) => {
res.send(`<h1>Resultats pour: ${req.query.q}</h1>`);
});
// SECURISE — echappement HTML
const escapeHtml = require('escape-html');
app.get('/search', (req, res) => {
const query = escapeHtml(req.query.q || '');
res.send(`<h1>Resultats pour: ${query}</h1>`);
});
XSS DOM via React¶
// VULNERABLE — dangerouslySetInnerHTML avec contenu non filtre
function Comment({ text }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />;
}
// SECURISE — rendu textuel ou DOMPurify pour le HTML intentionnel
import DOMPurify from 'dompurify';
function Comment({ text }) {
// Option 1 : texte pur (prefere si pas besoin de HTML)
return <div>{text}</div>;
// Option 2 : HTML sanitise si le HTML est necessaire
// return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }} />;
}
A10 — Server-Side Request Forgery (SSRF)¶
L'application fait des requêtes HTTP vers une URL controlee par l'attaquant — accès au réseau interne, metadata cloud, services internes.
import urllib.parse
import ipaddress
import requests
# VULNERABLE — fetch d'URL utilisateur sans validation
def fetch_url_bad(url):
response = requests.get(url) # http://169.254.169.254/latest/meta-data/ => AWS metadata
return response.text
# SECURISE — validation stricte de l'URL
ALLOWED_SCHEMES = {"https"}
BLOCKED_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0"}
def is_safe_url(url: str) -> bool:
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
return False
host = parsed.hostname or ""
if host in BLOCKED_HOSTS:
return False
try:
addr = ipaddress.ip_address(host)
if addr.is_private or addr.is_loopback or addr.is_link_local:
return False
except ValueError:
pass # hostname, pas une IP — continuer
return True
def fetch_url(url: str) -> str:
if not is_safe_url(url):
raise ValueError("URL non autorisee")
response = requests.get(url, timeout=5, allow_redirects=False)
return response.text
SSRF et cloud
En environnement cloud (AWS, GCP, Azure), le endpoint de metadata (169.254.169.254 ou fd00:ec2::254) est accessible depuis toute instance. Une SSRF peut exposer les credentials IAM de l'instance.
Synthèse : vulnérabilité, impact, prevention¶
| Vulnérabilité | Impact potentiel | Prevention principale |
|---|---|---|
| Broken Access Control | Accès a toutes les données | Vérifier ownership côté serveur |
| Cryptographic Failures | Vol de credentials, données en clair | bcrypt/Argon2, TLS 1.3, pas de MD5/SHA1 |
| SQL Injection | Dump complet de la DB, RCE | Requêtes parametrees, ORM |
| Command Injection | Exécution de code arbitraire | Pas de shell=True, whitelist des inputs |
| XSS | Vol de session, phishing, defacement | Echappement contextuel, CSP stricte |
| SSRF | Accès réseau interne, credentials cloud | Validation URL, blocklist IP privées |
Chapitre suivant : Gestion des secrets — stocker et rotater les credentials de façon sécurisée.