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é

Intent redirection sur Android : attaques et correctifs

Comment l'intent redirection permet d'atteindre des composants Android non exportés, de divulguer des données via setResult() et de détourner les PendingIntent, et six moyens de s'en protéger.

L'intent redirection est une classe de vulnérabilité des applications Android dans laquelle un acteur malveillant incite une application victime à transférer ou à envoyer un Intent en son nom. Comme l'intent transféré s'exécute sous l'identité et avec les permissions de l'application victime, l'attaquant peut atteindre des composants non exportés, voler des données sensibles ou élever ses privilèges — le tout sans détenir la moindre permission particulière.

Cette vulnérabilité figure systématiquement parmi les résultats les plus impactants des programmes de bug bounty Android et a touché des applications très exposées, dont TikTok et plusieurs applications internes de Google.


Comment fonctionne l'intent redirection

Au fond, l'attaque exploite un modèle proxy : une application victime reçoit un intent provenant d'une source externe (contrôlée par l'attaquant) et, faute de validation adéquate, utilise une partie de cet intent pour démarrer une autre activity, envoyer un broadcast ou se lier à un service.

Déroulement de l'attaque

L'intent redirection sur Android permet aux attaquants de détourner des composants proxy exportés pour transférer des Intents non fiables, atteindre des cibles non exportées, divulguer des données via setResult(), voler du contenu via des octrois d'URI et détourner des flux d'authentification — ainsi que des mesures d'atténuation pratiques en profondeur.

Le schéma d'abus de base

Le code vulnérable le plus simple ressemble à ceci :

// VULNERABLE — Unvalidated intent forwarding
Intent forward = getIntent().getParcelableExtra("next_intent");
startActivity(forward);

La victime fait aveuglément confiance à l'extra next_intent et lance le composant indiqué par l'attaquant — y compris les activities non exportées de la victime elle-même.


Schémas de code vulnérables

1. Transfert d'intent non validé

// VULNERABLE
class RouterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val target = intent.getParcelableExtra<Intent>("target")
        target?.let { startActivity(it) }   // No validation at all
        finish()
    }
}

2. Fuite de données via setResult()

// VULNERABLE — Returns internal data to the caller
public class LeakyActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent next = getIntent().getParcelableExtra("next");
        startActivityForResult(next, 1001);
    }

    @Override
    protected void onActivityResult(int req, int res, Intent data) {
        super.onActivityResult(req, res, data);
        // Forwards internal data straight back to the (attacker) caller
        setResult(res, data);
        finish();
    }
}
// VULNERABLE — Deep link handler forwards without checking scheme/host
class DeepLinkActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val uri = intent.data
        val redirect = Intent(Intent.ACTION_VIEW, uri)
        // Attacker can craft: myapp://redirect?url=intent://...
        startActivity(redirect)
    }
}

Scénarios d'attaque

Scénario 1 — Accéder à des composants non exportés

Un attaquant construit un intent ciblant l'InternalSettingsActivity non exportée de la victime :

Intent inner = new Intent();
inner.setComponent(new ComponentName(
    "com.victim.app",
    "com.victim.app.InternalSettingsActivity"
));

Intent outer = new Intent();
outer.setComponent(new ComponentName(
    "com.victim.app",
    "com.victim.app.RouterActivity"  // exported proxy
));
outer.putExtra("target", inner);
startActivity(outer);

Scénario 2 — Vol de données via content provider

L'attaquant exploite les octrois de permission sur URI pour lire le content provider privé de la victime :

val inner = Intent().apply {
    data = Uri.parse("content://com.victim.app.provider/private_data")
    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}

val outer = Intent().apply {
    component = ComponentName("com.victim.app", "com.victim.app.ProxyActivity")
    putExtra("next_intent", inner)
}
startActivityForResult(outer, 0)
// onActivityResult receives the private data

Scénario 3 — Détournement d'authentification / de session

Si l'application victime possède une activity exportée qui transfère un résultat issu d'un flux de connexion interne, l'attaquant peut intercepter le jeton d'authentification renvoyé via setResult().

Un attaquant enchaîne plusieurs deep links à travers différentes applications, utilisant chacune d'elles comme tremplin pour finalement atteindre un composant protégé dans une cible à forte valeur.

Détournement de PendingIntent

Alors que l'intent redirection classique exploite une application qui transfère un Intent fourni par l'attaquant sous sa propre identité, le détournement de PendingIntent inverse l'attaque : l'application victime est amenée à remettre un PendingIntent que l'attaquant peut transformer en arme. Comme un PendingIntent exécute l'Intent qu'il encapsule avec l'UID et les permissions de son créateur, un attaquant qui obtient un PendingIntent mutable ou vide emprunte de fait l'identité de l'application victime.

Comprendre le modèle de sécurité des PendingIntent

Un PendingIntent est un jeton de capacité (capability token), pas un objet de données. L'Intent, les flags et l'UID du créateur résident dans system_server ; l'application ne détient qu'un handle binder.

  • Créateur : appelle PendingIntent.getActivity() → system_server enregistre l'entrée et renvoie un handle.
  • Destinataire : reçoit le handle via IPC (extra d'Intent, notification, etc.).
  • Envoi : lorsque .send() est appelé, system_server recherche l'entrée et déclenche l'Intent encapsulé avec l'UID du créateur, pas celui de l'émetteur.

l'UID du créateur et l'Intent ne quittent jamais system_server — le destinataire ne détient qu'un handle binder opaque

Cette conception rend le PendingIntent sûr à transmettre par défaut — c'est d'ailleurs pourquoi il constitue la mesure d'atténuation recommandée contre l'intent redirection classique. La vulnérabilité n'apparaît que lorsque le créateur traite ce jeton de capacité comme une simple donnée inoffensive.

Le schéma vulnérable

Le détournement de PendingIntent survient lorsqu'une application victime :

  1. Crée un PendingIntent avec FLAG_MUTABLE (ou omet les flags sur les cibles antérieures à Android 12, où mutable était la valeur par défaut), ET
  2. Encapsule un Intent implicite (sans composant explicite défini), ET
  3. Expose le PendingIntent à un attaquant — en le plaçant dans un broadcast, une action de notification, la réponse d'un service exporté, ou un Intent renvoyé à un appelant.

Un attaquant qui reçoit le PendingIntent mutable peut appeler .send(Context, int, Intent fillInIntent) avec un fillInIntent fournissant le composant, l'action, les données ou les extras manquants. Le système fusionne les champs du fillIn dans l'Intent d'origine selon les règles définies dans Intent.fillIn(), puis envoie le résultat avec l'UID du créateur.

Les conséquences pratiques comprennent :

  • Lancer les activities, services ou providers non exportés de la victime.
  • Lire ou écrire les URI content:// privées de la victime en accordant FLAG_GRANT_READ_URI_PERMISSION via le fillIn.
  • Envoyer des broadcasts en tant que victime vers des receivers qui font confiance à la signature ou au nom de package de la victime.
  • Déclencher des flux internes privilégiés (gestion de compte, modifications de paramètres, callbacks d'achat intégré).

Un exemple concret

// Vulnerable code inside VictimApp
Intent intent = new Intent();                         // implicit — no component
PendingIntent pi = PendingIntent.getActivity(
    this, 0, intent,
    PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

Intent deliver = new Intent("com.victim.HAND_OUT_TOKEN");
deliver.putExtra("token", pi);
sendBroadcast(deliver);                               // attacker receives pi

Le receiver de l'attaquant obtient le PendingIntent et l'exploite :

// Inside AttackerApp
PendingIntent pi = intent.getParcelableExtra("token", PendingIntent.class);

Intent fillIn = new Intent();
fillIn.setClassName("com.victim", "com.victim.internal.AdminActivity");
fillIn.putExtra("cmd", "wipe");

pi.send(context, 0, fillIn);      // launches VictimApp's internal activity as VictimApp

Même si AdminActivity n'est pas exportée, le lancement réussit car le système l'exécute sous l'UID de VictimApp.

En quoi cela diffère de l'intent redirection classique

Aspect Intent redirection classique Détournement de PendingIntent
Ce que fournit l'attaquant Un Intent brut à la victime Rien — l'attaquant reçoit un jeton de la part de la victime
Rôle de la victime Extrait un Intent et l'envoie Remet un PendingIntent encapsulant un Intent implicite et mutable
Identité sous laquelle l'Intent s'exécute La victime (confused deputy) La victime (délégation de capacité)
Mesure d'atténuation principale Valider/filtrer l'Intent transféré ; utiliser un PendingIntent à la place Utiliser FLAG_IMMUTABLE ; utiliser des Intent explicites

Les dégâts sont les mêmes — le code s'exécute en tant que victime — mais la forme de l'attaque est inversée. L'intent redirection classique nécessite un chemin d'entrée par lequel la victime accepte des données de l'attaquant. Le détournement de PendingIntent nécessite un chemin de sortie par lequel la victime divulgue une capacité.

Stratégies d'atténuation

1. Valider avec resolveActivity()

Avant de transférer un intent, vérifiez qu'il résout vers un composant sûr et attendu :

val forwarded = intent.getParcelableExtra<Intent>("next")
forwarded?.let {
    val resolved = it.resolveActivity(packageManager)
    if (resolved != null && resolved.packageName == packageName) {
        // GOOD — only allow intents targeting our own package
        startActivity(it)
    }
}

2. Liste blanche de composants

Maintenez un ensemble explicite de composants cibles autorisés :

private val ALLOWED_TARGETS = setOf(
    "com.myapp.HomeActivity",
    "com.myapp.SettingsActivity"
)

fun safeForward(intent: Intent) {
    val target = intent.getParcelableExtra<Intent>("target") ?: return
    val comp = target.component?.className
    if (comp in ALLOWED_TARGETS) {
        startActivity(target)
    }
}

3. Utiliser IntentSanitizer (AndroidX)

L'API Jetpack IntentSanitizer propose une API déclarative, de type builder, pour supprimer les champs dangereux :

val sanitizer = IntentSanitizer.Builder()
    .allowComponent(ComponentName(this, HomeActivity::class.java))
    .allowAction(Intent.ACTION_VIEW)
    .allowDataWithAuthority("myapp.example.com")
    .allowExtra("safe_key", String::class.java)
    .build()

val clean = sanitizer.sanitizeByFiltering(untrustedIntent)
startActivity(clean)

4. Restreindre les composants exportés

Passez en revue votre AndroidManifest.xml et assurez-vous que les composants ne sont exportés que lorsque c'est réellement nécessaire :

<!-- GOOD — not exported; cannot be reached externally -->
<activity
    android:name=".InternalSettingsActivity"
    android:exported="false" />

<!-- If exported is required, protect with a permission -->
<activity
    android:name=".RouterActivity"
    android:exported="true"
    android:permission="com.myapp.permission.INTERNAL" />

5. PendingIntent immuables

Utilisez toujours FLAG_IMMUTABLE, sauf si le PendingIntent doit réellement être mutable :

val pi = PendingIntent.getActivity(
    context, 0, intent,
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)

6. Supprimer les flags d'Intent dangereux

Avant de transférer un intent, retirez les flags susceptibles d'accorder des permissions sur URI :

fun stripDangerousFlags(intent: Intent): Intent {
    intent.removeFlags(
        Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
        Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
    )
    return intent
}

Idées reçues

Idée reçue Réalité
« Mettre android:exported=false rend mon composant sûr. » Si une activity proxy exportée lui transfère des intents, le composant reste effectivement atteignable.
« Je valide l'action de l'intent, donc je suis protégé. » L'attaquant contrôle tous les champs — composant, URI de données, extras, flags. Ne valider que l'action est insuffisant.
« Les contrôles de permission sur le composant cible bloqueront l'attaquant. » L'intent transféré s'exécute sous l'identité de l'application victime, qui détient déjà les permissions requises.
« Seul startActivity() est vulnérable. » sendBroadcast(), startService(), bindService() et startActivityForResult() sont tout aussi exposés.
« Les deep links sont sûrs car ils ne font qu'ouvrir des URL web. » Le schéma intent:// permet de construire des intents arbitraires à partir d'une URI, contournant les hypothèses habituelles sur les URL.

Impact dans le monde réel

  • TikTok (2022) : des chercheurs ont démontré qu'une chaîne d'intent redirections pouvait permettre la prise de contrôle de comptes en atteignant des activities non exportées gérant les jetons d'authentification. Cette découverte a donné lieu à une récompense de bug bounty significative.
  • Bulletins de sécurité Google : plusieurs Android Security Bulletins ont traité des failles d'intent redirection dans des composants système, rappelant que même le code first-party n'est pas à l'abri.
  • Tendances en bug bounty : l'intent redirection figure systématiquement parmi les principales catégories de vulnérabilités Android sur des plateformes comme HackerOne et Bugcrowd, avec des récompenses à la hauteur de son impact élevé.

Conclusion : la défense en profondeur

Aucun correctif unique n'élimine le risque d'intent redirection. Une approche en couches est indispensable :

  1. Minimiser les exports — n'exporter que les composants qui nécessitent réellement un accès externe.
  2. Valider tous les intents transférés — utiliser resolveActivity(), des listes blanches de composants ou IntentSanitizer.
  3. Supprimer les flags dangereux — retirer les flags d'octroi d'URI avant tout transfert.
  4. Utiliser des PendingIntent immuables — privilégier FLAG_IMMUTABLE par défaut.
  5. Protéger les résultats sensibles — ne jamais renvoyer de données internes via setResult() à des appelants non vérifiés.
  6. Intégrer l'analyse statique — des outils comme Android Lint et Semgrep peuvent détecter les mauvaises configurations en CI/CD avant la mise en production.

En traitant tout intent reçu de l'extérieur comme une entrée non fiable et en appliquant ces contrôles de façon systématique, les développeurs Android peuvent neutraliser efficacement l'intent redirection comme vecteur d'attaque.