Retour au cours

cyber / cybersecurite-fondamentale

Sécurité des API

Leçon 171 exercice

Explication

Ce que vous allez apprendre

  • Distinguer clairement authentification (qui es-tu) et autorisation (qu'as-tu le droit de faire)
  • Reconnaître et corriger un mass assignment via des schémas d'entrée stricts (extra = "forbid")
  • Adapter le rate limiting selon le coût et la sensibilité réelle de chaque route
  • Vérifier explicitement l'algorithme attendu lors du décodage d'un JWT pour éviter l'attaque "alg=none"
  • Éviter l'accumulation d'API fantômes grâce à un versionnement et une dépréciation clairement documentés

Dans quel contexte ?

Une API permet à un utilisateur de mettre à jour son profil via PATCH /users/me en acceptant directement le JSON envoyé. Un attaquant ajoute discrètement le champ "role": "admin" à sa requête. Si le schéma d'entrée ne rejette pas les champs inattendus, ce champ est silencieusement accepté et l'utilisateur se retrouve administrateur du système, sans qu'aucune vulnérabilité "visible" n'ait été exploitée : c'est un mass assignment, une des failles les plus sournoises du Top 10 API OWASP.

Une synthèse plutôt qu'un sujet nouveau

Cette leçon rassemble les principes vus jusqu'ici (authentification, moindre privilège, injection, rate limiting) et les applique spécifiquement au contexte des API, qui sont aujourd'hui la porte d'entrée principale de la majorité des applications modernes (frontend web, mobile, intégrations tierces).

Authentification n'est pas autorisation

C'est la confusion la plus fréquente chez les développeurs débutants : vérifier qu'un utilisateur EST bien connecté (authentification) ne dit rien sur ce qu'il a le DROIT de faire (autorisation). Un endpoint qui vérifie seulement la présence d'un jeton valide, sans vérifier les permissions associées, laisse n'importe quel utilisateur connecté accéder à des actions réservées aux administrateurs.

Le mass assignment, un piège silencieux

Piège fréquent

Quand une API accepte directement un objet JSON envoyé par le client et le convertit tel quel en objet de base de données, elle prend un risque énorme : un attaquant peut ajouter des champs non prévus dans sa requête (comme "role": "admin" ou "is_verified": true) et les voir acceptés silencieusement si le schéma d'entrée n'interdit pas explicitement les champs inattendus. La bonne pratique est de définir des schémas d'entrée stricts qui rejettent tout champ non prévu.

Pourquoi le rate limiting doit être différencié

Toutes les routes n'ont pas le même coût ni le même risque.

Type de routeRisque principalStratégie de limite
Login / authentificationBrute force, credential stuffingLimite basse par IP + par compte
Recherche en base de donnéesCoût CPU/DB élevéLimite modérée + pagination obligatoire
Export de données volumineusesExfiltration, déni de serviceLimite très basse + file d'attente

Appliquer une limite uniforme partout laisse souvent les routes les plus sensibles sous-protégées.

Le versionnement d'API comme discipline de sécurité

Bonne pratique

Garder d'anciennes versions d'API actives indéfiniment, sans dépréciation claire, crée des "API fantômes" : des routes oubliées, moins surveillées, souvent moins bien sécurisées que la version actuelle. Documentez et planifiez systématiquement la fin de vie de chaque version d'API exposée.

Commandes & code

Sécurité des API

text
# OWASP API Security Top 10 (extrait pertinent)
API1 : Broken Object Level Authorization (= IDOR appliqué aux API, voir leçon dédiée)
API2 : Broken Authentication
API3 : Broken Object Property Level Authorization (exposition de champs sensibles)
API4 : Unrestricted Resource Consumption (absence de rate limiting)
API5 : Broken Function Level Authorization (accès à des endpoints admin sans contrôle de rôle)
API8 : Security Misconfiguration
API9 : Improper Inventory Management (API "fantômes" non documentées, oubliées)
python
# Authentification API par JWT : structure et vérification correcte
import jwt
from datetime import datetime, timedelta, timezone

JWT_SECRET = settings.jwt_secret  # chargé depuis vault/env, jamais en dur
ALGORITHM = "HS256"

def create_access_token(user_id: int) -> str:
    payload = {
        "sub": str(user_id),
        "iat": datetime.now(timezone.utc),
        "exp": datetime.now(timezone.utc) + timedelta(minutes=15),  # durée de vie courte
        "type": "access",
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=ALGORITHM)

def verify_access_token(token: str) -> dict:
    try:
        return jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM])  # spécifier explicitement l'algo attendu
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Token expiré")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Token invalide")

# Piège classique : ne PAS spécifier "algorithms=[...]" ouvre à l'attaque "alg=none"
# où un attaquant forge un JWT avec l'algorithme "none" pour contourner la vérification de signature
python
# Validation stricte des entrées avec Pydantic (rejette tout ce qui n'est pas explicitement attendu)
from pydantic import BaseModel, EmailStr, Field, field_validator

class CreateUserRequest(BaseModel):
    email: EmailStr
    username: str = Field(min_length=3, max_length=32, pattern=r"^[a-zA-Z0-9_-]+$")
    age: int = Field(ge=13, le=120)

    class Config:
        extra = "forbid"   # rejette tout champ non déclaré (empêche le mass assignment / injection de champs)

    @field_validator("username")
    @classmethod
    def username_not_reserved(cls, v):
        if v.lower() in {"admin", "root", "system"}:
            raise ValueError("nom d'utilisateur réservé")
        return v
python
# Mass assignment : ne JAMAIS binder directement l'entrée utilisateur sur un modèle DB complet
# VULNÉRABLE : l'utilisateur peut injecter "role": "admin" ou "is_verified": true dans le JSON
@app.post("/users")
async def create_user_vulnerable(payload: dict, db=Depends(get_db)):
    user = User(**payload)  # accepte AVEUGLÉMENT tous les champs fournis
    db.add(user)

# CORRECT : passer par un schéma d'entrée explicite qui ne contient QUE les champs autorisés
@app.post("/users")
async def create_user_safe(payload: CreateUserRequest, db=Depends(get_db)):
    user = User(email=payload.email, username=payload.username, role="viewer")  # role forcé côté serveur
    db.add(user)
python
# Rate limiting par utilisateur/IP avec slowapi (wrapper de limits pour FastAPI)
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.get("/api/search")
@limiter.limit("30/minute")   # limite spécifique à cet endpoint coûteux
async def search(request: Request, q: str):
    ...

@app.post("/api/export")
@limiter.limit("5/hour")       # endpoints lourds : quotas plus stricts
async def export_data(request: Request):
    ...
yaml
# Documentation exhaustive de l'inventaire API (contre API9 - Improper Inventory Management)
# OpenAPI généré automatiquement par FastAPI, mais à compléter avec le versionnement explicite
openapi: 3.0.0
info:
  title: Technologik API
  version: "2.3.0"
paths:
  /api/v1/courses:
    get:
      deprecated: false
  /api/v1/legacy/courses:
    get:
      deprecated: true   # endpoint legacy à surveiller/retirer, jamais laisser "oublié" en production
text
# Checklist sécurité API avant mise en production
- Chaque endpoint vérifie l'authentification ET l'autorisation (pas seulement l'authentification)
- Les schémas Pydantic utilisent "extra = forbid", jamais de binding direct dict -> modèle DB
- Rate limiting en place sur les endpoints sensibles/coûteux (recherche, export, login)
- Versionnement explicite des routes (/api/v1/...) avec dépréciation documentée des anciennes versions
- Aucune donnée sensible (mot de passe hashé, secrets) dans les réponses JSON, même par erreur

Résumé

  • Toujours spécifier l'algorithme JWT attendu à la vérification (algorithms=[...]), jamais l'accepter dynamiquement.
  • Le mass assignment se contre avec des schémas d'entrée stricts (extra = "forbid"), jamais un binding direct.
  • Le rate limiting doit être différencié par endpoint selon son coût réel (login, export, recherche).
  • Documenter et déprécier explicitement les anciennes versions d'API évite les "API fantômes" oubliées.

Exercices pratiques

1 disponible
1

Mission : l'utilisateur qui s'est auto-promu administrateur

Objectif : Identifier une faille de mass assignment à partir d'une requête réseau et corriger le schéma d'entrée en conséquence.

Contexte

Un utilisateur standard de Technologik envoie cette requête à PATCH /users/me :

json
{"display_name": "Alex", "role": "admin"}

Après cet appel, il découvre qu'il a désormais accès aux fonctions d'administration. Le endpoint utilise aujourd'hui payload: dict et fait User(**payload) directement sur le modèle de base de données.

Résoudre l’exercice →