Garden of KnowledgeApplied Sciences › Computer Science › Software › Security › Web Security
March 22, 2026

API Security

Les APIs (REST, GraphQL, gRPC) constituent la colonne vertébrale des applications modernes. Elles représentent une surface d’attaque critique car elles exposent directement la logique métier et les données.

OWASP API Security Top 10 (2023)§

#VulnérabilitéDescription
API1Broken Object Level AuthorizationAccéder aux objets d’un autre utilisateur via leur ID
API2Broken AuthenticationTokens faibles, sessions non invalidées
API3Broken Object Property Level AuthorizationRetourner des champs sensibles non nécessaires
API4Unrestricted Resource ConsumptionPas de rate limiting → DoS, coût excessif
API5Broken Function Level AuthorizationAccéder à des endpoints admin sans les droits
API6Unrestricted Access to Sensitive Business FlowsAchat en masse, exploitation de workflows
API7Server-Side Request ForgerySSRF via des paramètres d’URL dans l’API
API8Security MisconfigurationCORS trop permissif, méthodes inutiles actives
API9Improper Inventory ManagementVersions obsolètes exposées (/v1 encore actif)
API10Unsafe Consumption of APIsFaire confiance aveuglément aux APIs tierces

API1 — Broken Object Level Authorization (BOLA/IDOR)§

La vulnérabilité la plus fréquente. L’API n’vérifie pas que l’utilisateur est propriétaire de la ressource demandée.

GET /api/v1/users/1337/orders/9876
Authorization: Bearer <token_user_42>

→ Si le serveur retourne les commandes de l'user 1337 sans vérifier
  que le token appartient à cet utilisateur → BOLA

Exploitation :

# Itérer sur les IDs pour accéder aux données d'autres utilisateurs
for user_id in range(1, 10000):
    r = requests.get(f"https://api.exemple.com/users/{user_id}/data",
                     headers={"Authorization": f"Bearer {my_token}"})
    if r.status_code == 200:
        print(f"User {user_id}: {r.json()}")

Défense :

# Toujours vérifier côté serveur
@app.route("/api/orders/<order_id>")
@require_auth
def get_order(order_id):
    order = Order.query.get(order_id)
    if order.user_id != current_user.id:  # Vérification obligatoire
        return {"error": "Forbidden"}, 403
    return order.to_dict()

# Utiliser des UUIDs plutôt que des IDs séquentiels
# → rend l'énumération difficile mais ne remplace pas la vérification

API5 — Broken Function Level Authorization§

Accès à des fonctions réservées aux admins depuis un compte normal.

# Utilisateur normal essaie des endpoints admin
DELETE /api/v1/admin/users/42
PUT /api/v1/users/42/role {"role": "admin"}
GET /api/v1/admin/logs
POST /api/v1/internal/migrate-db

Outils de découverte d’endpoints :

# Fuzzing d'endpoints
ffuf -u https://api.exemple.com/FUZZ \
     -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
     -H "Authorization: Bearer $TOKEN"

# Chercher les specs OpenAPI/Swagger
GET /swagger.json
GET /api-docs
GET /openapi.yaml
GET /v1/spec

API4 — Rate Limiting et Resource Consumption§

# Flask — Rate limiting avec Flask-Limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(app, key_func=get_remote_address)

@app.route("/api/login", methods=["POST"])
@limiter.limit("5 per minute")  # Max 5 tentatives/min par IP
def login():
    ...

@app.route("/api/search")
@limiter.limit("100 per hour")
def search():
    ...

GraphQL — Prévention des attaques par complexité de requête :

# Limiter la profondeur et la complexité des requêtes
from graphql import build_schema
from graphql_depth_limit import depth_limit_validator

schema = build_schema(...)
# Limiter à 5 niveaux de profondeur
validation_rules = [depth_limit_validator(5)]

Sécurité GraphQL§

GraphQL présente des risques spécifiques par rapport à REST.

Introspection§

Par défaut, GraphQL expose son schéma complet via l’introspection.

# Récupérer le schéma entier
curl -X POST https://api.exemple.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{__schema{types{name fields{name type{name}}}}}"}'

Désactiver en production :

# Python graphene
schema = graphene.Schema(query=Query, auto_camelcase=False)
app.add_url_rule('/graphql', view_func=GraphQLView.as_view(
    'graphql',
    schema=schema,
    introspection=False  # Désactivé en production
))

Injection GraphQL§

# Injection dans un argument GraphQL
query {
  user(name: "admin' OR '1'='1") {  # SQLi si mal géré
    email
    password
  }
}

Batching d’attaques§

GraphQL permet plusieurs requêtes en une seule requête HTTP, contournant le rate limiting par IP.

[
  {"query": "mutation { login(user: \"admin\", pass: \"pass1\") { token } }"},
  {"query": "mutation { login(user: \"admin\", pass: \"pass2\") { token } }"},
  {"query": "mutation { login(user: \"admin\", pass: \"pass3\") { token } }"}
]

Défense : limiter à une requête par batch, ou implémenter un rate limiting par utilisateur (pas seulement IP).

Authentification et autorisation API§

Bearer Tokens — Bonnes pratiques§

# Génération sécurisée
import secrets
token = secrets.token_urlsafe(32)  # 256 bits d'entropie

# Validation
def validate_token(token: str) -> Optional[User]:
    # Utiliser une comparaison en temps constant (anti-timing attack)
    stored_token = db.get_token(token[:8])  # Lookup partiel
    if not stored_token:
        return None
    if not secrets.compare_digest(stored_token.value, token):
        return None
    if stored_token.expires_at < datetime.utcnow():
        return None
    return stored_token.user

API Keys — Stockage et rotation§

# Format recommandé pour API keys
prefix_randompart
# ex: sk_live_kJ8mPqR9xN2vL5wE3fD7hA4cB6nM0pZ1
# Le préfixe permet d'identifier le type de clé dans les logs

# Hacher les API keys en base (comme des mots de passe)
import hashlib
key_hash = hashlib.sha256(api_key.encode()).hexdigest()

CORS — Configuration sécurisée§

# Flask-CORS
from flask_cors import CORS

# MAUVAIS
CORS(app, origins="*")  # Toutes les origines

# BIEN — liste blanche explicite
CORS(app, origins=[
    "https://app.mondomaine.com",
    "https://dashboard.mondomaine.com"
], supports_credentials=True)
// Node.js Express
app.use(cors({
  origin: (origin, callback) => {
    const whitelist = ['https://app.mondomaine.com'];
    if (!origin || whitelist.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 3600
}));

Pentest d’API — Méthodologie§

Découverte et cartographie§

# 1. Trouver la documentation
GET /swagger.json
GET /openapi.yaml
GET /api/v1/docs
GET /.well-known/openapi

# 2. Spider les endpoints depuis les apps mobiles
# Intercepter le trafic avec BurpSuite + appareil mobile/émulateur

# 3. Analyser les specs OpenAPI avec OWASP ZAP
zap-cli openapi-import --target https://api.exemple.com swagger.json

# 4. Générer des requêtes depuis la spec
docker run --rm -v $PWD:/tmp willbryant/openapi-fuzzer:latest \
  -s /tmp/swagger.json -t https://api.exemple.com

Tests manuels essentiels§

# Tester BOLA : accéder aux ressources d'un autre user
# Créer 2 comptes, noter les IDs, tester croisements

# Tester l'énumération d'objets
GET /api/users/1 200
GET /api/users/2 200
GET /api/users/3 404  # Vide
GET /api/users/4 200

# Tester les méthodes HTTP
OPTIONS /api/users/1 lister les méthodes permises
PATCH /api/users/1 {"role": "admin"}  # Mass assignment ?
DELETE /api/users/1  # Droit sur d'autres users ?

# Tester les paramètres cachés
POST /api/users
{"username": "test", "email": "[email protected]", "role": "admin"}  # Ignoré ?

Outils spécialisés API§

OutilUsage
PostmanTests manuels, collections
InsomniaAlternative Postman, REST/GraphQL
BurpSuiteProxy, scanner, répéteur
OWASP ZAPScanner automatique API
ArjunDécouverte de paramètres cachés
ffufFuzzing d’endpoints et paramètres
GraphQL VoyagerVisualisation du schéma GraphQL
InQLBurpSuite plugin pour GraphQL
# Arjun — découverte de paramètres cachés
arjun -u https://api.exemple.com/search -m GET

# ffuf — fuzzing de paramètres
ffuf -u "https://api.exemple.com/users?FUZZ=test" \
     -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
     -fs 0  # Filtrer les réponses vides

Gestion des erreurs et information leakage§

# MAUVAIS — expose les détails internes
@app.errorhandler(Exception)
def handle_error(e):
    return {
        "error": str(e),
        "traceback": traceback.format_exc(),  # Stack trace visible
        "query": current_query                 # Requête SQL visible
    }, 500

# BIEN — erreurs génériques en production
@app.errorhandler(Exception)
def handle_error(e):
    error_id = str(uuid.uuid4())
    logger.error(f"Error {error_id}: {e}", exc_info=True)  # Log interne
    return {
        "error": "Internal server error",
        "error_id": error_id  # Pour corrélation avec les logs
    }, 500
—The Gardener