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

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();
}
}
3. Explotación de deep links
// 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().
Escenario 4 — Encadenamiento de deep links
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_serveralmacena 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_serverbusca el registro y lanza el Intent envuelto con la UID del creador, no con la del remitente.

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:
- 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 - Envuelve un Intent implícito (sin un componente explícito establecido), Y
- 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 concediendoFLAG_GRANT_READ_URI_PERMISSIONa 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:
- Minimizar las exportaciones — exporte únicamente los componentes que realmente necesiten acceso externo.
- Validar todos los intents reenviados — use
resolveActivity(), listas de componentes permitidos oIntentSanitizer. - Eliminar los flags peligrosos — quite los flags de concesión de URI antes de reenviar.
- Usar PendingIntents inmutables — recurra a
FLAG_IMMUTABLEpor defecto. - Proteger los resultados sensibles — nunca devuelva datos internos mediante
setResult()a llamadores no verificados. - 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.