🔌
Sécurité réseau
Sécurité des APIs REST : les bonnes pratiques
📅 2025-04-09 ⏱ 10 min de lecture 🏷 Avancé

// SOMMAIRE

Sécurité des APIs REST : les bonnes pratiques

Les APIs REST sont le cœur des applications modernes. Elles connectent frontend, mobile, microservices et partenaires. Mais une API mal sécurisée est une porte d'entrée directe sur vos données et systèmes.

Pourquoi les APIs sont des cibles privilégiées

En 2023, 83% du trafic internet passe par des APIs. Les 10 principales vulnérabilités API (OWASP API Top 10) représentent les vecteurs d'attaque les plus courants.

OWASP API Top 10

API1 — Broken Object Level Authorization (BOLA)

La vulnérabilité la plus commune. L'API ne vérifie pas si l'utilisateur a le droit d'accéder à un objet spécifique.

# Requête légitime de l'utilisateur 123

GET /api/users/123/profile

Authorization: Bearer token_user_123

Attaque BOLA : accès au profil d'un autre utilisateur

GET /api/users/124/profile

Authorization: Bearer token_user_123

Si l'API retourne les données → BOLA !

Correction :

# Toujours vérifier que l'objet appartient à l'utilisateur connecté

@app.route('/api/users/<int:user_id>/profile')

def get_profile(user_id):

current_user = get_current_user() # Depuis le token JWT

# Vérification d'autorisation explicite

if current_user.id != user_id and not current_user.is_admin:

return jsonify({'error': 'Unauthorized'}), 403

return jsonify(User.query.get(user_id).to_dict())

API2 — Broken Authentication

# Token JWT sans expiration

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoxMjN9.

Algorithme "none" → pas de signature → facile à falsifier !

Token avec algorithme faible

eyJhbGciOiJIUzI1NiJ9...

Clé secrète faible : "secret" ou "password"

Bonnes pratiques :

import jwt

from datetime import datetime, timedelta

def create_token(user_id):

payload = {

'user_id': user_id,

'exp': datetime.utcnow() + timedelta(hours=1), # Expiration 1h

'iat': datetime.utcnow(),

'jti': str(uuid.uuid4()) # ID unique pour révocation

}

return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

# Utiliser RS256 (asymétrique) pour les systèmes critiques

API3 — Broken Object Property Level Authorization

# Requête normale de mise à jour du profil

PATCH /api/users/123

{"name": "Jean Martin", "email": "jean@example.com"}

Attaque : ajout de propriétés non attendues (Mass Assignment)

PATCH /api/users/123

{"name": "Jean", "email": "jean@example.com", "role": "admin", "balance": 99999}

Si l'API accepte "role" et "balance" → Élévation de privilèges !

Correction :

# Whitelisting des champs autorisés

ALLOWED_UPDATE_FIELDS = {'name', 'email', 'phone'}

def update_user(user_id, data):

# Ne traiter que les champs autorisés

filtered_data = {k: v for k, v in data.items() if k in ALLOWED_UPDATE_FIELDS}

User.query.filter_by(id=user_id).update(filtered_data)

API4 — Unrestricted Resource Consumption

# Sans rate limiting : l'attaquant peut :

- Envoyer 1 000 000 requêtes/seconde

- Télécharger des fichiers énormes

- Déclencher des opérations coûteuses en boucle

Avec Flask-Limiter

from flask_limiter import Limiter

limiter = Limiter(app, key_func=get_remote_address)

@app.route('/api/send-email')

@limiter.limit("5 per minute") # Max 5 emails par minute par IP

def send_email():

pass

@app.route('/api/search')

@limiter.limit("100 per hour")

def search():

pass

API5 — Broken Function Level Authorization

# Endpoints admin non protégés

GET /api/users # Liste publique → OK

POST /api/admin/users # Création admin → doit être protégé !

DELETE /api/admin/users/123 # Suppression → doit être protégé !

L'attaquant tente de découvrir les endpoints admin

GET /api/admin

GET /api/v1/admin

GET /api/internal

Sécurisation complète d'une API

Authentification et autorisation

from functools import wraps

import jwt

def require_auth(f):

@wraps(f)

def decorated(args, *kwargs):

token = request.headers.get('Authorization', '').replace('Bearer ', '')

if not token:

return jsonify({'error': 'Token manquant'}), 401

try:

payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])

request.current_user = User.query.get(payload['user_id'])

except jwt.ExpiredSignatureError:

return jsonify({'error': 'Token expiré'}), 401

except jwt.InvalidTokenError:

return jsonify({'error': 'Token invalide'}), 401

return f(args, *kwargs)

return decorated

def require_role(role):

def decorator(f):

@wraps(f)

def decorated(args, *kwargs):

if not request.current_user.has_role(role):

return jsonify({'error': 'Accès refusé'}), 403

return f(args, *kwargs)

return decorated

return decorator

@app.route('/api/admin/users')

@require_auth

@require_role('admin')

def list_all_users():

return jsonify([u.to_dict() for u in User.query.all()])

Headers de sécurité

@app.after_request

def add_security_headers(response):

response.headers['X-Content-Type-Options'] = 'nosniff'

response.headers['X-Frame-Options'] = 'DENY'

response.headers['Content-Security-Policy'] = "default-src 'self'"

response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'

response.headers['X-XSS-Protection'] = '1; mode=block'

# Ne jamais exposer la version du serveur

response.headers.pop('Server', None)

response.headers.pop('X-Powered-By', None)

return response

Validation des entrées

from marshmallow import Schema, fields, validate, ValidationError

class UserCreateSchema(Schema):

name = fields.Str(required=True, validate=validate.Length(min=2, max=50))

email = fields.Email(required=True)

age = fields.Int(validate=validate.Range(min=18, max=120))

role = fields.Str(validate=validate.OneOf(['user', 'moderator']))

# Jamais 'admin' accessible depuis l'API publique !

@app.route('/api/users', methods=['POST'])

@require_auth

def create_user():

try:

data = UserCreateSchema().load(request.json)

except ValidationError as e:

return jsonify({'errors': e.messages}), 400

# Continuer avec data validée et filtrée

Logging et monitoring

import logging

from datetime import datetime

security_logger = logging.getLogger('security')

@app.before_request

def log_request():

security_logger.info({

'timestamp': datetime.utcnow().isoformat(),

'ip': request.remote_addr,

'method': request.method,

'path': request.path,

'user_agent': request.user_agent.string

})

Alertes sur comportements suspects

def check_suspicious_activity(user_id, action):

recent_failures = get_recent_failures(user_id, minutes=5)

if recent_failures > 10:

security_logger.warning(f"Activité suspecte user {user_id}: {recent_failures} échecs")

block_user_temporarily(user_id)

Checklist de sécurité API

Authentification

✅ JWT avec expiration courte (1h max)

✅ Algorithme RS256 ou HS256 (jamais "none")

✅ Refresh tokens avec rotation

✅ Révocation de tokens possible

Autorisation

✅ Vérification objet par objet (anti-BOLA)

✅ Whitelisting des champs modifiables

✅ Endpoints admin séparés et protégés

Données

✅ Validation stricte des entrées

✅ Pas de données sensibles dans les URLs

✅ Pagination limitée (max 100 résultats)

✅ HTTPS uniquement (HSTS activé)

Infrastructure

✅ Rate limiting par endpoint

✅ Logs de sécurité centralisés

✅ Headers de sécurité

✅ Tests de pénétration réguliers

Conclusion

La sécurité d'une API repose sur le principe "ne jamais faire confiance au client". Validez tout, autorisez explicitement, limitez le débit et loguez tout. Les outils comme OWASP ZAP ou Burp Suite permettent d'auditer automatiquement vos APIs.

💬 Voir l'article avec commentaires →
← Ingénierie sociale : manipuler l'humain Linux pour la sécurité : commandes essentielles →