Aller au contenu

Sécuriser un contrat API-first dès la conception

Sécuriser un contrat API-first dès la conception

Section intitulée « Sécuriser un contrat API-first dès la conception »

Une API (Application Programming Interface, interface de programmation applicative) n’est pas un détail d’intégration : c’est une frontière de confiance exposée à des clients, des services et, tôt ou tard, à des erreurs de conception. En API-first, le contrat doit définir ce qui est autorisé, refusé et traçable avant que le code ne transforme des suppositions en dette de sécurité.

→ Lire l’explication complète de la marmotte

Le compromis central est brutal : un contrat strict ralentit les changements improvisés, mais il empêche les consommateurs d’inventer des comportements que le service devra ensuite supporter éternellement. La recommandation est nette : traiter le contrat comme une politique de sécurité versionnée, validée en intégration continue, puis appliquée à l’entrée du service. Un fichier OpenAPI (OpenAPI Specification, spécification de description d’API) décoratif ne protège rien. Il meuble très bien une slide, ce qui est déjà une carrière respectable dans certains produits.

Le contrat doit fixer, pour chaque opération :

  • l’identité attendue et le mécanisme d’authentification ;
  • les droits requis, exprimés comme des capacités métier ;
  • les formats de requête et de réponse ;
  • les bornes de taille, de cardinalité et de pagination ;
  • les codes d’erreur observables ;
  • les données interdites dans les réponses ;
  • les règles d’idempotence, c’est-à-dire le comportement d’une répétition de requête.

Une opération PATCH /users/{id} sans schéma fermé accepte souvent des propriétés inattendues. C’est la voie classique vers la mass assignment vulnerability : le client soumet isAdmin, tenantId ou creditLimit, puis le framework associe mécaniquement les champs reçus au modèle interne. Le contrat doit déclarer explicitement les propriétés admises et le code doit construire une commande métier dédiée. Désérialiser un objet de domaine depuis Internet revient à laisser un formulaire remplir la comptabilité, les badges d’accès et le thermostat.

Les formats doivent être canoniques là où une signature, un cache ou une comparaison porte sur le contenu. Deux représentations JSON (JavaScript Object Notation, format textuel de données) sémantiquement proches peuvent différer par l’ordre des champs, l’encodage ou la précision numérique. Pour les règles de sérialisation, les signatures et la cryptographie applicative, consulter aussi les formats, bibliothèques et API cryptographiques.

Le contrat ne doit pas exposer les modèles internes. Un identifiant technique, un état de workflow ou un champ d’audit peut être légitime dans une base, mais ne l’est pas automatiquement dans une réponse. Concevoir un modèle de lecture public distinct coûte des transformations. Ce coût est inférieur à celui d’une compatibilité accidentelle devenue contractuelle, puis impossible à retirer sans comité de crise et tableur maudit.

Authentification et autorisation au niveau de l’opération

Section intitulée « Authentification et autorisation au niveau de l’opération »

L’authentification répond à « quelle identité présente la requête ? ». L’autorisation répond à « cette identité peut-elle faire cette action sur cette ressource ? ». Confondre les deux produit des API où un jeton valide devient un passe-partout. Le jeton n’est pas une permission universelle, même s’il est signé avec beaucoup d’assurance.

Pour les accès délégués, OAuth 2.0 (Open Authorization version 2.0) doit produire des jetons à durée courte, destinés à une audience précise et porteurs de droits limités. Un JWT (JSON Web Token, jeton JSON signé) doit être vérifié sur au minimum sa signature, son émetteur, son audience, son instant d’expiration et son instant de validité. Accepter un algorithme indiqué par l’en-tête du jeton sans liste locale d’algorithmes autorisés ouvre la porte aux confusions d’algorithme. La vérification doit imposer l’algorithme attendu et sélectionner la clé par un identifiant contrôlé.

Les droits doivent représenter une action métier, par exemple invoice:approve, et non une route technique telle que POST:/v1/invoices/{id}. Une route évolue ; l’intention de sécurité doit survivre à cette évolution. L’autorisation doit aussi contrôler la ressource ciblée : un droit invoice:read ne donne pas accès à toutes les factures de tous les tenants. Cette vérification de propriété doit être exécutée après résolution de la ressource et avant lecture ou mutation.

Le contrôle d’accès doit être cohérent entre API publique, tâches asynchrones et interfaces d’administration. Déplacer une opération sensible dans une file de messages ne fait pas disparaître l’autorisation, cela la déplace. Le consommateur doit recevoir l’identité ou les attributs nécessaires, protégés contre l’altération, puis réévaluer la règle. Les sujets d’identité fédérée et de cycle de vie des comptes relèvent de la gestion des identités.

Validation des entrées et surface d’attaque HTTP

Section intitulée « Validation des entrées et surface d’attaque HTTP »

HTTP (Hypertext Transfer Protocol, protocole de transfert hypertexte) transporte plus que le corps JSON. Les en-têtes, paramètres de chemin, paramètres de requête, encodages, méthodes et tailles sont tous des entrées non fiables. Le contrat doit borner chacun d’eux. Une limite de 100 éléments par page, une longueur maximale de 128 caractères pour un filtre et un plafond de corps à 1 mégaoctet constituent des contrôles de disponibilité, pas de la décoration documentaire.

Valider au proxy ou à la passerelle réduit la charge inutile, mais ne dispense pas le service de valider. La passerelle peut être contournée par un réseau interne, une tâche de maintenance ou une mauvaise règle de routage. La validation de bord et la validation métier ont des responsabilités différentes : la première filtre les formes illégales, la seconde vérifie les états autorisés.

Les attaques fréquentes suivent un motif stable :

  • BOLA (Broken Object Level Authorization) : modifier un identifiant de ressource et lire les données d’un autre tenant. Contre-mesure : autoriser chaque objet, pas seulement la route.
  • Injection : transmettre une expression de filtre, une requête ou une structure interprétable. Contre-mesure : grammaire limitée, requêtes paramétrées et aucune concaténation vers un interpréteur.
  • Rejeu : répéter une mutation après une coupure réseau ou depuis un journal compromis. Contre-mesure : clé d’idempotence liée à l’identité, à l’opération et au corps canonique, avec durée de rétention définie.
  • Énumération : parcourir des identifiants prévisibles ou distinguer les erreurs. Contre-mesure : identifiants non séquentiels exposés, contrôle d’accès systématique et réponses d’erreur non révélatrices.
  • Épuisement de ressources : envoyer des corps compressés, des tableaux énormes ou des recherches coûteuses. Contre-mesure : limites avant décompression, délais, quotas et coût maximal de requête.

Le chiffrement de transport protège le trajet, pas le serveur qui reçoit la requête. TLS (Transport Layer Security, protocole de sécurité des communications) et l’authentification mutuelle sont traités dans la sécurisation des canaux TLS. Une connexion chiffrée vers une opération sans autorisation reste une fuite avec un joli cadenas.

L’exemple suivant ferme le schéma de création, impose une portée fonctionnelle et documente une clé d’idempotence. Le validateur doit être placé dans la chaîne de requête, avant le contrôleur métier. Générer seulement un client à partir de ce fichier n’applique aucune de ces règles côté serveur.

# openapi.yaml : OpenAPI 3.1, à valider avec un validateur compatible JSON Schema
openapi: 3.1.0
info:
title: Billing API
version: 1.0.0
paths:
/v1/invoices:
post:
security:
- oauth2: [invoice:create] # Le jeton doit porter ce droit fonctionnel.
parameters:
- in: header
name: Idempotency-Key
required: true
schema:
type: string
minLength: 16
maxLength: 128
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false # Refuse tenantId, status ou tout champ non déclaré.
required: [customerId, amount, currency]
properties:
customerId:
type: string
pattern: '^[a-zA-Z0-9_-]{16,64}$'
amount:
type: integer
minimum: 1
maximum: 100000000
currency:
type: string
enum: [EUR, CHF]
responses:
"201":
description: Facture créée
"400":
description: Requête invalide
"403":
description: Droit ou appartenance insuffisants

La clé d’idempotence ne doit pas être un identifiant global réutilisable. Stocker son empreinte avec l’identité, l’opération, l’empreinte du corps et la réponse produite. Si la même clé arrive avec un corps différent, renvoyer un conflit, pas une seconde interprétation créative. La conservation doit couvrir la fenêtre réaliste de rejeu, puis être purgée. Sans purge, l’idempotence devient un entrepôt de données personnel, avec facture et regrets.

Une modification incompatible exige une nouvelle version ou une période de coexistence. Supprimer un champ de réponse, changer sa sémantique ou rendre obligatoire un attribut auparavant facultatif casse des clients, même si les tests du fournisseur sont verts. Le versionnement par média ou par chemin dépend surtout de la capacité des passerelles et des consommateurs à router ces variantes ; l’exigence reste identique : publier la durée de dépréciation et mesurer les appels restants avant retrait.

Les secrets de signature, certificats, jetons de service et clés d’API ne doivent jamais figurer dans le contrat, les exemples, les journaux ou les erreurs. Une rotation sans chevauchement casse les appels en vol ; une rotation avec chevauchement exige que le vérificateur accepte temporairement l’ancienne et la nouvelle clé, identifiées sans ambiguïté. Les clés privées doivent rester dans un gestionnaire de secrets ou un HSM (Hardware Security Module, module matériel de sécurité) lorsque le niveau de risque le justifie.

La supervision doit corréler identifiant de requête, identité authentifiée, opération, décision d’autorisation, code de réponse et durée. Ne pas journaliser le jeton, le corps complet ou les données personnelles par défaut. Les alertes doivent suivre les refus d’autorisation, les erreurs de validation, les dépassements de quota, les échecs de signature et les appels à des versions dépréciées.

Le contrat ne garantit ni la sécurité du code métier, ni l’absence de fraude, ni la confidentialité des données stockées, ni la disponibilité face à une saturation réseau. Ces propriétés viennent respectivement des revues et tests de sécurité, des contrôles métier, du chiffrement et de la gestion des clés, puis de l’architecture réseau et de capacité. Une spécification n’est pas un talisman. C’est une règle exécutable, ce qui est déjà beaucoup plus rare.

Sans contrat d’API qui définit à l’avance ce qui est autorisé, refusé et tracé, chaque implémentation improvise ses propres règles d’accès. Les suppositions non écrites se transforment en dette de sécurité : un point d’entrée oublié reste exposé, un champ qui ne devait jamais sortir fuit dans une réponse, une méthode non prévue est acceptée faute d’avoir été explicitement interdite. La frontière de confiance devient floue, car rien ne dit quel appelant a le droit de faire quoi ni ce qui doit être journalisé. La conséquence est une surface d’attaque découverte au fil des incidents plutôt qu’au moment de la conception, et des contrôles incohérents d’un service à l’autre parce que chaque gardien interprète le règlement à sa façon. Une porte non documentée finit par donner accès à des données que personne n’avait décidé d’exposer.

Une interface de programmation permet à un programme de demander quelque chose à un autre programme. Le contrat décrit à l’avance les demandes acceptées : les chemins disponibles, les informations attendues, leur format, leur taille et les réponses possibles. Il doit aussi indiquer quelle identité peut appeler chaque action et quelles actions cette identité a le droit de réaliser.

Quand une demande arrive, le système vérifie d’abord sa forme. Un champ inconnu, un texte trop long ou un nombre hors limite doit être refusé. Ensuite, il vérifie l’identité présentée par la demande. Cette identité est généralement prouvée par un jeton signé et limité dans le temps. Le système contrôle alors le droit demandé et vérifie que la ressource visée appartient bien au bon utilisateur ou au bon groupe.

Pour une action qui modifie une donnée, une clé de répétition évite de créer deux fois le même résultat si la demande est renvoyée après une panne réseau. Le système mémorise alors le résultat pendant une durée définie.

Le contrat doit évoluer avec prudence : retirer un champ ou changer son sens peut casser les programmes qui l’utilisent. Les secrets servant à prouver les identités doivent être renouvelés sans les écrire dans les journaux. Enfin, les événements de refus, d’erreur et de surcharge doivent être surveillés. Le contrat réduit les erreurs d’accès, mais il ne corrige pas seul un programme métier défaillant, une base de données mal protégée ou un réseau indisponible.

À lire aussi

Pages voisines