L’OWASP Top 10 est la référence pour la sécurité applicative web. Ces vulnérabilités sont responsables de la majorité des incidents de sécurité — les connaître et savoir les corriger est le minimum pour tout développeur.
A01 — Broken Access Control
La vulnérabilité la plus répandue : un utilisateur peut accéder aux ressources d’un autre utilisateur.
# ❌ Vulnérable — l'user_id vient de la requête, pas de la session
@app.get("/orders/{order_id}")
async def get_order(order_id: int, user_id: int = Query(...)):
return db.get_order(order_id) # n'importe qui peut lire n'importe quelle commande
# ✅ Corrigé — vérifier que la ressource appartient à l'utilisateur authentifié
@app.get("/orders/{order_id}")
async def get_order(order_id: int, current_user: User = Depends(get_current_user)):
order = db.get_order(order_id)
if order.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Accès refusé")
return order
A02 — Cryptographic Failures
Données sensibles exposées par une cryptographie faible ou absente.
# ❌ Vulnérable — MD5 est cassé depuis 1996
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()
# ❌ Aussi vulnérable — SHA1 également compromis
hashed = hashlib.sha1(password.encode()).hexdigest()
# ✅ Corrigé — bcrypt ou argon2 avec salt automatique
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
hashed = pwd_context.hash(password) # hash + salt automatique
valid = pwd_context.verify(password, hashed) # vérification
A03 — Injection
SQL, NoSQL, LDAP, OS — tout input non validé injecté dans une requête.
# ❌ Injection SQL classique
def get_user(username: str):
query = f"SELECT * FROM users WHERE username = '{username}'"
# username = "admin'--" → contourne l'authentification
cursor.execute(query)
# ✅ Requêtes paramétrées
def get_user(username: str):
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
# ❌ Injection de commande OS
import subprocess
def ping_host(host: str):
output = subprocess.run(f"ping -c 1 {host}", shell=True)
# host = "8.8.8.8; cat /etc/passwd"
# ✅ Passer les arguments séparément, jamais shell=True avec input utilisateur
def ping_host(host: str):
if not re.match(r'^[\d.]+$', host):
raise ValueError("IP invalide")
output = subprocess.run(["ping", "-c", "1", host], capture_output=True)
A04 — Insecure Design
Des failles dans la conception, pas l’implémentation. Exemple : un système de reset de mot de passe qui envoie le nouveau mot de passe par email en clair — même parfaitement implémenté, la conception est vulnérable.
Bonnes pratiques de conception :
- Modélisation des menaces (threat modeling) dès la phase de design
- Défense en profondeur — plusieurs couches de sécurité
- Fail secure — en cas d’erreur, refuser l’accès plutôt que l’autoriser
A05 — Security Misconfiguration
# ❌ Vulnérable — debug mode en production, credentials par défaut
DEBUG=True
SECRET_KEY=django-insecure-abc123
DATABASE_PASSWORD=admin
# ❌ Headers de sécurité manquants (vérifiable avec securityheaders.com)
# Pas de X-Frame-Options, Content-Security-Policy, etc.
# ✅ Configuration sécurisée
DEBUG=False
SECRET_KEY=${SECRET_KEY_FROM_VAULT}
ALLOWED_HOSTS=sphinx-digital.com
# ✅ Headers de sécurité dans FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["sphinx-digital.com"])
@app.middleware("http")
async def security_headers(request, call_next):
response = await call_next(request)
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Strict-Transport-Security"] = "max-age=31536000"
return response
A07 — Identification and Authentication Failures
# ❌ Vulnérable — pas de limite de tentatives, JWT sans expiration
@app.post("/login")
async def login(credentials: LoginForm):
user = db.get_user(credentials.username)
if user and user.password == credentials.password: # comparaison en clair
token = jwt.encode({"sub": user.id}, SECRET_KEY) # pas d'expiration
return {"token": token}
# ✅ Corrigé
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
@app.post("/login")
@limiter.limit("5/minute") # max 5 tentatives par minute par IP
async def login(request: Request, credentials: LoginForm):
user = db.get_user(credentials.username)
# Timing-safe pour éviter les timing attacks
if not user or not pwd_context.verify(credentials.password, user.hashed_password):
raise HTTPException(status_code=401)
token = jwt.encode({
"sub": str(user.id),
"exp": datetime.utcnow() + timedelta(hours=1) # expiration obligatoire
}, SECRET_KEY, algorithm="HS256")
return {"token": token}
A09 — Security Logging and Monitoring Failures
import structlog
logger = structlog.get_logger()
@app.post("/login")
async def login(request: Request, credentials: LoginForm):
client_ip = request.client.host
try:
user = authenticate(credentials)
logger.info("login.success",
user_id=user.id,
ip=client_ip,
user_agent=request.headers.get("user-agent"))
return create_token(user)
except AuthenticationError:
logger.warning("login.failure", # WARN, pas ERROR — c'est normal
username=credentials.username, # username mais JAMAIS le mot de passe
ip=client_ip,
reason="invalid_credentials")
raise HTTPException(status_code=401)
Notre formation Cybersécurité couvre l’OWASP Top 10 avec des ateliers pratiques sur des applications volontairement vulnérables.