Neutron, nuestro motor de IA, obtuvo un 96.75% en el benchmark CyberGym de UC Berkeley. Más información

Seguridad

Seguridad

Redirección de intents en Android: ataques y soluciones

Cómo la redirección de intents permite a los atacantes alcanzar componentes no exportados de Android, filtrar datos mediante setResult() y abusar de los PendingIntent, además de seis formas de prevenirla.

La redirección de intents es una clase de vulnerabilidad en las aplicaciones Android en la que un actor malicioso engaña a la aplicación víctima para que reenvíe o despache un Intent en su nombre. Como el intent reenviado se ejecuta bajo la identidad y los permisos de la aplicación víctima, el atacante puede alcanzar componentes no exportados, robar datos sensibles o escalar privilegios, todo ello sin disponer de ningún permiso especial propio.

Esta vulnerabilidad se sitúa de forma constante entre los hallazgos de mayor impacto en los programas de bug bounty de Android y ha afectado a aplicaciones de alto perfil, incluidas TikTok y varias aplicaciones propias de Google.


Cómo funciona la redirección de intents

En esencia, el ataque explota un patrón proxy: una aplicación víctima recibe un intent procedente de una fuente externa (controlada por el atacante) y, sin una validación adecuada, usa parte de ese intent para iniciar otra actividad, enviar una difusión (broadcast) o enlazarse a un servicio.

Flujo del ataque

Intent redirection in Android lets attackers abuse exported proxy components to forward untrusted Intents, reach unexported targets, leak data via setResult(), steal content via URI grants, and hijack auth flows—plus practical, defense-in-depth mitigations.

El patrón de uso indebido central

El código vulnerable más simple tiene este aspecto:

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

La víctima confía ciegamente en el extra next_intent y lanza cualquier componente que especifique el atacante, incluidas las propias actividades no exportadas de la víctima.


Patrones de código vulnerable

1. Reenvío de intents sin validar

// 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. Fuga de datos mediante 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)
    }
}

Escenarios de ataque

Escenario 1 — Acceso a componentes no exportados

Un atacante crea un intent dirigido a la InternalSettingsActivity no exportada de la víctima:

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);

Escenario 2 — Robo de datos de content provider

El atacante aprovecha las concesiones de permisos de URI para leer el content provider privado de la víctima:

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

Escenario 3 — Secuestro de autenticación o de sesión

Si la aplicación víctima tiene una actividad exportada que reenvía un resultado procedente de un flujo de inicio de sesión interno, el atacante puede interceptar el token de autenticación devuelto mediante setResult().

Un atacante encadena varios deep links a través de distintas aplicaciones, usando cada una como peldaño para alcanzar finalmente un componente protegido en un objetivo de alto valor.

Redirección de PendingIntent

Mientras que la redirección de intents clásica explota una aplicación que reenvía un Intent proporcionado por el atacante bajo su propia identidad, la redirección de PendingIntent invierte el ataque: se engaña a la aplicación víctima para que entregue un PendingIntent que el atacante puede convertir en arma. Como un PendingIntent ejecuta el Intent que envuelve con la UID y los permisos del creador, un atacante que obtiene un PendingIntent mutable o vacío toma prestada, en la práctica, la identidad de la aplicación víctima.

Cómo entender el modelo de seguridad de PendingIntent

Un PendingIntent es un token de capacidad, no un objeto de datos. El Intent, los flags y la UID del creador residen dentro de system_server; la aplicación solo mantiene un handle de binder.

  • Creador: llama a PendingIntent.getActivity() → system_server almacena el registro y devuelve un handle.
  • Receptor: recibe el handle por IPC (un extra de un Intent, una notificación, etc.).
  • Despacho: cuando se llama a .send(), system_server busca el registro y lanza el Intent envuelto con la UID del creador, no con la del remitente.

the creator UID and Intent never leave system_server — the recipient only holds an opaque binder handle

Este diseño hace que el PendingIntent sea seguro de compartir por defecto, razón por la cual es la mitigación recomendada frente a la redirección de intents clásica. La vulnerabilidad surge únicamente cuando el creador trata este token de capacidad como si fuera un dato inofensivo.

El patrón vulnerable

La redirección de PendingIntent ocurre cuando una aplicación víctima:

  1. Crea un PendingIntent con FLAG_MUTABLE (u omite los flags en apps dirigidas a versiones anteriores a Android 12, donde mutable era el valor predeterminado), Y
  2. Envuelve un Intent implícito (sin un componente explícito establecido), Y
  3. Expone el PendingIntent a un atacante, colocándolo en una difusión, una acción de notificación, la respuesta de un servicio exportado o un Intent devuelto a quien lo llama.

Un atacante que recibe el PendingIntent mutable puede llamar a .send(Context, int, Intent fillInIntent) con un fillInIntent que suministre el componente, la acción, los datos o los extras que faltan. El sistema combina los campos de fillIn con el Intent original según las reglas de Intent.fillIn(), y despacha el resultado con la UID del creador.

El impacto práctico incluye:

  • Lanzar las actividades, servicios o providers no exportados de la víctima.
  • Leer o escribir las URIs content:// privadas de la víctima concediendo FLAG_GRANT_READ_URI_PERMISSION a través del fillIn.
  • Enviar difusiones como la víctima a receptores que confían en la firma o el nombre de paquete de la víctima.
  • Activar flujos internos privilegiados (gestión de cuentas, cambios de configuración, callbacks de compras dentro de la app).

Un ejemplo concreto

// 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

El receiver del atacante obtiene el PendingIntent y lo explota:

// 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

Aunque AdminActivity no está exportada, el lanzamiento se produce con éxito porque el sistema lo ejecuta bajo la UID de VictimApp.

Por qué es distinta de la redirección de intents clásica

Aspecto Redirección de intents clásica Redirección de PendingIntent
El atacante proporciona Un Intent en bruto a la víctima Nada — el atacante recibe un token de la víctima
Rol de la víctima Extrae un Intent y lo despacha Entrega un PendingIntent que envuelve un Intent implícito y mutable
Identidad bajo la que se ejecuta el Intent La víctima (confused deputy) La víctima (delegación de capacidad)
Mitigación principal Validar/filtrar el Intent reenviado; usar PendingIntent en su lugar Usar FLAG_IMMUTABLE; usar Intents explícitos

El daño es el mismo — el código se ejecuta como la víctima —, pero la forma del ataque está invertida. La redirección clásica requiere una vía de entrada en la que la víctima acepta datos del atacante. La redirección de PendingIntent requiere una vía de salida en la que la víctima filtra una capacidad.

Estrategias de mitigación

1. Validar con resolveActivity()

Antes de reenviar cualquier intent, verifique que resuelve a un componente seguro y esperado:

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. Lista de componentes permitidos

Mantenga un conjunto explícito de componentes de destino permitidos:

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. Usar IntentSanitizer (AndroidX)

La API IntentSanitizer de Jetpack ofrece una API declarativa, de tipo builder, para eliminar los campos peligrosos:

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. Restringir los componentes exportados

Revise su AndroidManifest.xml y asegúrese de que los componentes solo se exportan cuando es realmente necesario:

<!-- 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. PendingIntents inmutables

Use siempre FLAG_IMMUTABLE a menos que el PendingIntent necesite ser mutable de verdad:

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

6. Eliminar flags de Intent peligrosos

Antes de reenviar, elimine los flags que podrían conceder permisos de 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
}

Conceptos erróneos habituales

Concepto erróneo Realidad
«Establecer android:exported=false hace seguro mi componente». Si una actividad proxy exportada le reenvía intents, el componente es, en la práctica, accesible.
«Valido la acción del intent, así que estoy protegido». Los atacantes controlan todos los campos: componente, URI de datos, extras, flags. Validar solo la acción es insuficiente.
«Las comprobaciones de permisos en el componente de destino bloquearán al atacante». El intent reenviado se ejecuta bajo la identidad de la propia aplicación víctima, que ya posee los permisos necesarios.
«Solo startActivity() es vulnerable». sendBroadcast(), startService(), bindService() y startActivityForResult() son todos susceptibles.
«Los deep links son seguros porque solo abren URLs web». El esquema intent:// permite construir intents arbitrarios a partir de una URI, eludiendo las suposiciones habituales sobre URLs.

Impacto en el mundo real

  • TikTok (2022): investigadores demostraron que una cadena de redirecciones de intents podía secuestrar cuentas de usuario al alcanzar actividades no exportadas que gestionaban tokens de autenticación. El hallazgo obtuvo un pago significativo en un programa de bug bounty.
  • Boletines de seguridad de Google: varios boletines de seguridad de Android han corregido fallos de redirección de intents en componentes del propio sistema, lo que confirma que ni siquiera el código de primera parte está exento.
  • Tendencias en bug bounty: la redirección de intents aparece de forma constante entre las principales categorías de vulnerabilidades de Android en plataformas como HackerOne y Bugcrowd, con pagos que reflejan su alto impacto.

Conclusión: defensa en profundidad

Ninguna corrección aislada elimina el riesgo de redirección de intents. Es imprescindible un enfoque por capas:

  1. Minimizar las exportaciones — exporte únicamente los componentes que realmente necesiten acceso externo.
  2. Validar todos los intents reenviados — use resolveActivity(), listas de componentes permitidos o IntentSanitizer.
  3. Eliminar los flags peligrosos — quite los flags de concesión de URI antes de reenviar.
  4. Usar PendingIntents inmutables — recurra a FLAG_IMMUTABLE por defecto.
  5. Proteger los resultados sensibles — nunca devuelva datos internos mediante setResult() a llamadores no verificados.
  6. Integrar análisis estático — herramientas como Android Lint y Semgrep pueden detectar configuraciones incorrectas en el CI/CD antes de que lleguen a producción.

Tratando cada intent recibido externamente como entrada no confiable y aplicando estos controles de forma consistente, los desarrolladores de Android pueden neutralizar eficazmente la redirección de intents como vector de ataque.