Aller au contenu

Attaque par confusion de scope OAuth

Un client OAuth mal enregistré ne compromet pas forcément son propre compte de service. Il peut compromettre le périmètre entier que son jeton est autorisé à parcourir. Demander read write « au cas où » transforme un vol de jeton banal en accès transversal aux données.

→ Lire l’explication complète de la marmotte

OAuth 2.0 est un cadre d’autorisation déléguée, malgré son nom historique qui n’est pas un acronyme développé officiellement. Un scope est une permission textuelle demandée par un client et accordée par le serveur d’autorisation. Le jeton d’accès obtenu transporte ce périmètre, soit dans son contenu, soit dans l’état conservé côté serveur.

Le compromis central est brutal : un scope générique réduit les changements de configuration lors d’une évolution fonctionnelle, mais augmente le rayon d’explosion de chaque fuite de jeton. Le gain est du confort de développement. Le prix est une autorisation excessive, persistante jusqu’à l’expiration ou à la révocation du jeton. Ce compromis doit être tranché contre le scope large pour les accès aux données métier, sans exception décorative ajoutée sur une slide d’architecture.

Une application de génération de rapports n’a pas à obtenir documents:write, users:read et admin. Son besoin est reports:read, éventuellement limité à une ressource, une organisation ou un propriétaire. Si le serveur de ressources accepte documents:read pour servir les rapports, le modèle de permission est déjà trop grossier. L’application n’est pas le problème, elle ne fait qu’exposer le défaut de modélisation.

Le scope n’est pas une identité. Il n’indique pas quel utilisateur ou quel service agit, mais ce que le jeton peut demander au serveur de ressources. Un contrôle d’accès robuste vérifie donc au minimum quatre dimensions : l’émetteur du jeton, son destinataire, son expiration et ses permissions. Un jeton signé avec les bons scopes mais destiné à une autre interface de programmation applicative, ou API (Application Programming Interface), doit être refusé.

Les erreurs de validation de aud (audience, destinataire) et iss (issuer, émetteur) aggravent immédiatement cette attaque. Un serveur qui accepte un jeton destiné à une autre API convertit un scope déjà trop large en passe-partout inter-service. Les pièges de validation des jetons sont détaillés dans l’authentification des personnes et services.

Démonstration : jeton de rapports, accès aux dossiers hors périmètre

Section intitulée « Démonstration : jeton de rapports, accès aux dossiers hors périmètre »

La démonstration suivante utilise Keycloak comme serveur d’autorisation et un serveur de ressources de laboratoire exposant deux ensembles : /reports/weekly et /documents/hr-salaries. Le client reporting-job ne doit lire que le premier. Il reçoit pourtant le scope documents:read, qui couvre les deux. Ce scénario doit rester isolé : jamais de jeton réel dans un terminal partagé, un historique shell ou un ticket. Les secrets adorent les exports CSV, les exports CSV adorent finir là où ils ne devraient pas.

Le client doit être configuré avec le flux client_credentials, destiné aux appels machine à machine. L’authentification utilisateur n’intervient donc pas ici : le vol du secret client, d’un jeton en mémoire, d’une variable d’environnement ou d’une trace réseau suffit.

Fenêtre de terminal
# Demander un jeton au serveur OAuth de laboratoire.
# Le client demande volontairement un droit trop large.
TOKEN=$(curl -fsS \
-u 'reporting-job:change-me-in-lab-only' \
-d 'grant_type=client_credentials' \
-d 'scope=reports:read documents:read' \
'https://auth.lab.example/realms/demo/protocol/openid-connect/token' \
| jq -r '.access_token')
# Usage fonctionnel attendu : lecture du rapport hebdomadaire.
curl -fsS \
-H "Authorization: Bearer ${TOKEN}" \
'https://api.lab.example/reports/weekly'
# Exploitation après vol du même jeton : données RH hors fonction.
# Cette requête doit échouer dans une configuration correcte.
curl -i \
-H "Authorization: Bearer ${TOKEN}" \
'https://api.lab.example/documents/hr-salaries'

Dans la configuration vulnérable, la dernière requête répond 200 OK. L’attaquant n’élève aucun privilège à l’exécution, ne casse aucune signature et ne contourne aucune authentification. Il réutilise une permission accordée légitimement au mauvais client. C’est moins spectaculaire qu’une chaîne d’exploitation avec quinze logos, mais nettement plus fréquent.

La correction minimale est de retirer documents:read du client et de limiter la politique du serveur de ressources à reports:read pour cette route. Une correction complète va plus loin : définir un scope orienté capacité, par exemple reports.weekly:read, et refuser qu’un scope de lecture documentaire général donne accès aux rapports. Les scopes par verbe métier coûtent davantage à gouverner, mais empêchent qu’une taxonomie paresseuse devienne une frontière de sécurité imaginaire.

Le test de non-régression doit conserver la seconde requête. Tester uniquement que le rapport est accessible prouve le chemin heureux, ce célèbre terrain de jeu où les contrôles d’accès fonctionnent toujours admirablement. Tester que les salaires sont inaccessibles prouve la frontière.

Surface d’attaque des jetons et des scopes larges

Section intitulée « Surface d’attaque des jetons et des scopes larges »

Le jeton peut être volé à plusieurs endroits. Les journaux applicatifs enregistrent parfois l’en-tête Authorization lors d’une erreur. Les collecteurs de traces copient des en-têtes HTTP (Hypertext Transfer Protocol) par défaut. Les variables d’environnement sont lisibles par un processus de débogage, un conteneur voisin mal isolé ou une interface d’administration. Un navigateur peut exposer un jeton injecté dans le stockage local après une vulnérabilité de script intersite.

Un JSON Web Token, ou JWT, signé n’est pas chiffré par nature. Son contenu est généralement décodable par toute personne qui le possède. Y placer des données personnelles ou des permissions détaillées ne les rend pas secrètes. La signature protège contre la modification, pas contre la lecture ni contre le rejeu pendant sa période de validité.

Les contre-mesures dépendent du type de client. Pour un client serveur, conserver le secret dans un gestionnaire de secrets, ne jamais le placer dans l’image de conteneur, restreindre sa lecture au processus concerné et préférer une authentification par certificat avec TLS mutuel, ou mTLS (mutual Transport Layer Security), lorsque le serveur d’autorisation le supporte. Pour une application native ou web, utiliser Authorization Code avec PKCE (Proof Key for Code Exchange) et ne pas traiter un secret intégré au logiciel comme un secret. Un secret distribué à tous les postes est un mot de passe imprimé sur des milliers de badges.

La durée de vie courte réduit la fenêtre de rejeu, mais ne corrige pas un scope excessif. Un jeton valable cinq minutes avec admin reste une mauvaise idée très efficace pendant cinq minutes. Lier les jetons à une audience précise, appliquer des indicateurs de ressource, contrôler l’émetteur et refuser les scopes non attendus côté API limitent le déplacement latéral. La défense doit exister au serveur de ressources, pas seulement dans la console d’enregistrement du client.

Cette attaque ne garantit pas l’accès à une donnée protégée par une règle supplémentaire. Une autorisation métier fondée sur le propriétaire, la classification de la donnée ou une approbation transactionnelle peut refuser l’opération. À l’inverse, compter sur cette règle métier pour compenser documents:read est un pari fragile : toute nouvelle route oubliée réintroduit l’accès. Les mécanismes de gestion des identités et les contrôles applicatifs doivent se compléter, pas se suppléer par télépathie.

Audit des clients OAuth et révocation des permissions inutiles

Section intitulée « Audit des clients OAuth et révocation des permissions inutiles »

L’audit utile ne consiste pas à exporter la liste des clients une fois par an pour la déposer dans un dossier nommé final_v7. Il compare trois sources : le scope accordé au client, le scope demandé dans les flux réels, et le besoin fonctionnel documenté avec un propriétaire identifié.

Pour chaque client, établir une matrice contenant l’identifiant du client, le type de flux, les audiences autorisées, les scopes accordés, les routes réellement appelées, le propriétaire technique et la date de dernière utilisation. Toute permission non justifiée par une route, ou toute route non rattachée à un besoin, est à supprimer. Le silence documentaire n’est pas une autorisation implicite.

Les journaux du serveur d’autorisation doivent enregistrer le client, le grant utilisé, les scopes demandés et accordés, l’audience, le résultat et un identifiant de corrélation. Les journaux du serveur de ressources doivent enregistrer le sujet, le client autorisé lorsqu’il est disponible, le scope évalué, la route et la décision. Ne pas journaliser le jeton brut : un système de supervision n’a pas à devenir le coffre-fort involontaire de l’attaquant.

La révocation d’un scope dans l’enregistrement d’un client ne retire pas nécessairement les droits des jetons déjà émis. Avec des JWT autoportés, le serveur de ressources peut continuer à les accepter jusqu’à expiration s’il ne consulte pas un état de révocation. Pour une coupure immédiate, invalider le client, révoquer les jetons ou augmenter une version d’autorisation vérifiée par l’API. Cette exigence impose soit une introspection en ligne, soit une liste de révocation distribuée, soit des jetons très courts. Le choix dépend de la tolérance à l’indisponibilité du serveur d’autorisation et du délai acceptable de coupure.

Les sauvegardes de configuration OAuth doivent être chiffrées, limitées et testées en restauration. Restaurer un serveur d’autorisation sans ses clés de signature, ses certificats, ses secrets clients ou sa base de révocation produit soit une panne générale, soit une réémission désordonnée de droits. Les deux ont tendance à réveiller les mêmes personnes, mais pas avec le même niveau de sérénité.

Sans scopes restreints par capacité, une lecture abusive des données hors fonction produit dans les journaux du serveur de ressources une décision « autorisé » indiscernable d’un appel légitime : même client, même scope évalué, même résultat. Aucune alerte fondée sur les refus ne se déclenche, et la détection ne peut reposer que sur une analyse comportementale des routes appelées, rarement en place. Après la découverte d’une fuite, l’analyse d’impact ne peut pas se limiter aux routes que le client utilise réellement : le périmètre présumé exposé couvre tout ce que le scope permettait d’atteindre pendant la durée de validité du jeton. Toute ressource ajoutée ultérieurement sous le même scope entre en outre dans ce périmètre sans modification du client, ce qui étend silencieusement l’exposition de jetons déjà émis.

Une application peut demander au système d’autorisation le droit d’accéder à certaines données. Elle présente son identité, puis reçoit un jeton d’accès si le système accepte sa demande. Ce jeton est une chaîne de caractères que l’application envoie ensuite au service qui contient les données. Le service lit les droits associés au jeton et décide si la demande est acceptée.

Le problème apparaît quand l’application demande plus de droits que nécessaire. Une application qui ne doit lire qu’un rapport peut obtenir aussi le droit de lire tous les documents d’une entreprise. Tant que son jeton reste secret, l’erreur peut passer inaperçue. Si une personne malveillante récupère ce jeton dans un journal, une sauvegarde, la mémoire d’un programme ou une configuration mal protégée, elle peut envoyer les mêmes demandes que l’application.

Le service de données ne sait pas forcément que le jeton a été volé. Il vérifie seulement que le jeton est valide, qu’il vient du bon système, qu’il est destiné au bon service et que le droit demandé est présent. Si le droit est trop large, il autorise une action hors du besoin initial.

La correction consiste à donner à chaque application uniquement les droits nécessaires, à vérifier régulièrement les droits utilisés et à retirer ceux qui ne servent plus. En cas de fuite, il faut aussi invalider rapidement les jetons déjà distribués.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.