SécuritéDevDevSecOpsOWASP

OWASP Top 10 2025 : les vulnérabilités web critiques et comment les corriger

11 octobre 2026 · Sphinx-Digital

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.