Chapitre 8 - Sécurité et bonnes pratiques

L'authentification

Authentification ou autorisation ?

Au bar : montrer sa carte de membre, c'est l'authentification. Le videur qui vérifie que la carte donne accès au carré VIP, c'est l'autorisation.

HTTP étant sans état, le client doit prouver son identité à chaque requête, en général dans le header Authorization.

La clé d'API

Une longue chaîne aléatoire, propre à chaque client, envoyée dans un header.

curl http://api-ritif.portlatch.com/v1/me -H "X-API-Key: ak_7f3c9e1b2d4a"

Une clé d'API est un mot de passe : jamais dans le code, jamais dans git, jamais dans une URL (?api_key=... finit dans les logs et l'historique du navigateur). Côté serveur, on la stocke hachée, comme un mot de passe.

Basic Auth

L'identifiant et le mot de passe, encodés en Base64, dans le header Authorization.

curl -u neo:redpill https://api.example.com/v1/me
# envoie : Authorization: Basic bmVvOnJlZHBpbGw=

Base64 est un encodage, pas un chiffrement : echo bmVvOnJlZHBpbGw= | base64 -d affiche neo:redpill. Sans HTTPS, c'est un mot de passe en clair.

Sessions ou tokens ?

Session (cookie)Token (JWT…)
PrincipeLe serveur garde la session en mémoire ou en base, le client garde un identifiant dans un cookieLe client garde un jeton qui contient lui-même les informations, signé par le serveur
EnvoiAutomatique par le navigateur (Cookie)Manuel (Authorization: Bearer ...)
État côté serveurOuiNon : le serveur vérifie juste la signature
RévocationFacile : on supprime la sessionDifficile : le token reste valable jusqu'à son expiration
Adapté à…Un site web classiqueUne API, une application mobile, des microservices

JWT

Un JSON Web Token est composé de trois parties en Base64url, séparées par des points :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMiIsIm5pY2tuYW1lIjoibmVvIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzk0NjQ0MDAwfQ.7xQ...
└────────── header ──────────────────┘ └──────────────────────────── payload ─────────────────────────────────────────────────────┘ └ signature
// header
{"alg": "HS256", "typ": "JWT"}

// payload (les « claims »)
{"sub": "12", "nickname": "neo", "role": "customer", "exp": 1794644000}

Pour lire un payload :

import base64, json
json.loads(base64.urlsafe_b64decode(token.split(".")[1] + "=="))

Le site jwt.io décode les JWT visuellement. Ne collez jamais un vrai token de production dans un site tiers.

Avec API-ritif

# Échanger sa clé contre un JWT valable 15 minutes
curl -s -X POST http://api-ritif.portlatch.com/v1/auth/token -H "X-API-Key: $KEY"
# {"access_token": "eyJ...", "token_type": "Bearer", "expires_in": 900}

# L'utiliser
curl -i http://api-ritif.portlatch.com/v1/me -H "Authorization: Bearer $TOKEN"

Revenez dans 15 minutes : même requête, 401.

Les pièges

OAuth2 : « Se connecter avec GitHub »

OAuth2 permet à une application d'agir au nom d'un utilisateur sur un autre service, sans jamais connaître son mot de passe.

Utilisateur        Application            GitHub (serveur d'autorisation)
    │ « Se connecter avec GitHub »                 │
    │──────────────────▶│                          │
    │        redirection vers GitHub               │
    │◀──────────────────│                          │
    │  se connecte et accepte les permissions      │
    │─────────────────────────────────────────────▶│
    │        redirection avec un « code »          │
    │◀─────────────────────────────────────────────│
    │──────────────────▶│  échange code ─▶ token   │
    │                   │─────────────────────────▶│
    │                   │◀─────────── access token │
    │                   │  appels d'API avec le token
    │                   │──────▶ api.github.com

On n'implémente jamais OAuth2 soi-même : on utilise un fournisseur (Keycloak, Auth0, Entra ID…) et une bibliothèque éprouvée.

HTTPS et CORS

HTTPS : non négociable

En HTTP, tout circule en clair : l'URL, les headers, le body. Sur le Wi-Fi de l'école, n'importe qui avec Wireshark peut lire votre clé d'API, votre token ou votre mot de passe.

HTTPS, c'est HTTP dans un tunnel TLS, qui apporte :

Avec Let's Encrypt, un certificat est gratuit et se renouvelle automatiquement. Il n'y a plus aucune excuse.

Pour une API, ne redirigez pas HTTP vers HTTPS : refusez les requêtes HTTP. Quand la redirection arrive, la clé d'API est déjà passée en clair, et le client ne s'en rendra jamais compte.

CORS

La politique de même origine

Par sécurité, un navigateur interdit au JavaScript d'une page de lire les réponses d'une autre origine. L'origine, c'est le trio protocole + domaine + port :

PageAppelleMême origine ?
https://bar.examplehttps://bar.example/v1/cocktails✅
https://bar.examplehttps://api.bar.example/v1/cocktails❌ domaine différent
http://localhost:8080http://localhost:3000/v1/cocktails❌ port différent
http://bar.examplehttps://bar.example/v1/cocktails❌ protocole différent

Sans cette règle, n'importe quel site que vous visitez pourrait appeler, avec vos cookies, l'API de votre banque.

Le mécanisme CORS

CORS (Cross-Origin Resource Sharing) permet à une API d'autoriser certaines origines. Le navigateur envoie l'origine de la page dans le header Origin, et l'API répond si elle l'accepte :

GET /v1/cocktails HTTP/1.1
Origin: http://localhost:5173

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:5173

Pour les requêtes « non simples » (un DELETE, du JSON, un header X-API-Key…), le navigateur envoie d'abord une requête preflight en OPTIONS pour demander la permission :

OPTIONS /v1/orders HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-api-key

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: content-type, x-api-key

CORS est une protection du navigateur. curl, Bruno ou un script Python ne l'appliquent pas : c'est pour ça que tout marchait jusqu'ici. Et c'est pour ça que CORS ne protège pas votre API contre un attaquant : seule l'authentification le fait.

Manip : provoquer une erreur CORS

API-ritif n'autorise qu'une seule origine. Saurez-vous trouver laquelle ?

  1. Créez un fichier index.html :
    <!doctype html>
    <meta charset="utf-8">
    <title>Test CORS</title>
    <button id="go">Charger la carte</button>
    <pre id="out"></pre>
    <script>
      document.getElementById("go").onclick = async () => {
        const out = document.getElementById("out");
        try {
          const res = await fetch("http://api-ritif.portlatch.com/v1/cocktails");
          out.textContent = JSON.stringify(await res.json(), null, 2);
        } catch (e) {
          out.textContent = "Erreur : " + e.message;
        }
      };
    </script>
  2. Servez-le sur le port 8080 : python3 -m http.server 8080, ouvrez http://localhost:8080 et cliquez. Lisez l'erreur dans la console des DevTools.
  3. La même requête avec curl fonctionne. Comparez les headers de réponse avec et sans Origin :
    curl -i http://api-ritif.portlatch.com/v1/cocktails -H "Origin: http://localhost:8080"
  4. Indice : les développeurs front qui utilisent Vite connaissent bien le port 5173. Testez cette origine avec curl, puis servez la page sur ce port.
  5. Modifiez le script pour passer une commande (POST /v1/orders avec du JSON et votre X-API-Key). Dans l'onglet Réseau, repérez la requête OPTIONS envoyée avant le POST.

Configurer CORS sur son API

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_methods=["*"],
    allow_headers=["*"],
)

Access-Control-Allow-Origin: * autorise tout le monde. C'est acceptable pour une API publique en lecture seule, jamais pour une API qui utilise des cookies.

Le rate limiting

Une API sans limite finit toujours par recevoir trop de requêtes : un script en boucle infinie, un client trop enthousiaste, un concurrent qui aspire vos données, ou une attaque par déni de service. Le rate limiting limite le nombre de requêtes par client et par période.

Exemple de réponse d'un barman débordé : « reviens dans 10 s ».

Pourquoi limiter ?

Les algorithmes

AlgorithmePrincipe
Fenêtre fixe100 requêtes par minute, compteur remis à zéro à chaque minute. Simple, mais permet 200 requêtes en 2 secondes à cheval sur deux minutes.
Fenêtre glissante100 requêtes sur les 60 dernières secondes, à tout instant. Plus juste.
Seau à jetons (token bucket)Le client a un seau de jetons qui se remplit à vitesse constante. Chaque requête consomme un jeton. Autorise de petites rafales.

Ce que l'API doit répondre

HTTP/1.1 429 Too Many Requests
Retry-After: 10
Content-Type: application/problem+json

{"title": "Too Many Requests", "status": 429, "detail": "Le barman est débordé, reviens dans 10 s"}

Beaucoup d'API informent aussi le client avant qu'il atteigne la limite :

curl -i https://api.github.com/users/torvalds
x-ratelimit-limit: 60
x-ratelimit-remaining: 57
x-ratelimit-reset: 1794644000

Les headers X-RateLimit-* sont une convention répandue mais non standard. L'IETF travaille à un standard (RateLimit et RateLimit-Policy), déjà supporté par certaines bibliothèques.

Le mettre en place

# pip install slowapi
from fastapi import Request
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

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

@app.exception_handler(RateLimitExceeded)
async def on_rate_limit(request: Request, exc: RateLimitExceeded):
    response = problem(429, "Too Many Requests", "Le barman est débordé, reviens dans une minute.")
    response.headers["Retry-After"] = "60"
    return response

@app.get("/v1/cocktails")
@limiter.limit("60/minute")
def list_cocktails(request: Request):   # le paramètre request est obligatoire
    ...

Limiter par adresse IP est un début, mais tous les étudiants d'une même salle partagent souvent la même IP publique. Une API authentifiée limite plutôt par clé ou par utilisateur.

En production, le rate limiting est souvent assuré en amont de l'application : par le reverse proxy (Nginx, Traefik), une passerelle d'API ou un CDN.

L'OWASP API Security Top 10

L'OWASP (Open Worldwide Application Security Project) est une fondation qui publie des références en sécurité applicative. Son API Security Top 10 recense les risques les plus fréquents dans les API. La dernière édition date de 2023.

La plupart de ces failles ne demandent aucune compétence technique avancée pour être exploitées : juste curl et un peu de curiosité. Comme pendant la chasse au trésor.

#RisqueAu bar, ça donne…Se protéger
API1Broken Object Level Authorization (BOLA, aussi appelé IDOR)Changer l'id dans /orders/42 et lire la commande d'un autre clientVérifier à chaque requête que l'objet appartient à l'utilisateur
API2Broken AuthenticationUne route de connexion sans limite d'essais, des tokens qui n'expirent jamaisBibliothèques éprouvées, expiration, rate limiting sur l'authentification
API3Broken Object Property Level Authorization (BOPLA)Envoyer {"role": "bartender"} dans un PATCH /me et devenir barmanN'accepter et ne renvoyer que les champs prévus (schémas stricts)
API4Unrestricted Resource Consumption?per_page=1000000, ou 10 000 commandes par secondeLimites de pagination, de taille de body, rate limiting
API5Broken Function Level AuthorizationUn client qui appelle /admin/scoreboard et obtient une réponseContrôler le rôle sur chaque route sensible, refuser par défaut
API6Unrestricted Access to Sensitive Business FlowsUn bot qui commande tous les 404 Spritz dès leur retour en stock pour les revendreDétection des comportements automatisés, limites métier
API7Server Side Request Forgery (SSRF)Une route « importer la photo du cocktail depuis une URL » à qui l'on donne http://localhost:8080/adminListe blanche de domaines, interdire les adresses internes
API8Security MisconfigurationLes stack traces en production, CORS ouvert à tous, HTTP sans TLSConfiguration revue, erreurs neutres, HTTPS partout
API9Improper Inventory ManagementL'API v0 du stagiaire de 2009, oubliée mais toujours en ligneInventaire des versions et des environnements, décommissionnement
API10Unsafe Consumption of APIsFaire aveuglément confiance aux données renvoyées par l'API du fournisseur de citronsValider aussi ce qu'on reçoit des API tierces

Les trois à retenir

Si vous ne deviez en retenir que trois, ce sont celles qui concernent l'autorisation : API1, API3 et API5. Elles ne sont détectées ni par les scanners automatiques, ni par les tests qui « vérifient que ça marche ». Il faut se demander, pour chaque route : et si c'était quelqu'un d'autre qui appelait ?

Le réflexe à prendre en écrivant une route : « Qui a le droit d'appeler cette route ? Sur quels objets ? Avec quels champs ? »

Référence complète : OWASP API Security Top 10 – 2023.