Secuestro de deep links y app links: cómo una app maliciosa roba tu sesión

Secuestro de deep links y app links: cómo una app maliciosa roba tu sesión

En una aplicación móvil, un enlace no siempre abre una página web. Puede iniciar una pantalla concreta, completar una autenticación, aceptar una invitación, recuperar una cuenta o cargar contenido dentro de un WebView. Esa capacidad recibe el nombre de deep linking y es una de las formas más habituales de conectar el navegador, el correo, otras aplicaciones y el sistema operativo con una app instalada.

También es una frontera de seguridad. Los esquemas URI personalizados, como miapp://callback, no demuestran quién es el propietario del nombre. Otra aplicación instalada puede declarar el mismo esquema y competir por recibir el enlace. Si ese enlace contiene una respuesta OAuth, un token temporal o una acción sensible, el impacto puede ir mucho más allá de abrir la pantalla equivocada.

Los Android App Links y los iOS Universal Links solucionan gran parte del problema al vincular una aplicación con un dominio controlado por su propietario. Sin embargo, la verificación del dominio no valida los parámetros recibidos, no corrige una autorización débil y no convierte en segura una acción peligrosa. El enlace puede llegar a la aplicación correcta y seguir explotando su lógica interna.

En este artículo veremos cómo funciona el secuestro de deep links, qué ocurre en Android y en iOS, por qué OAuth es uno de los escenarios más delicados, cómo se auditan los enlaces verificados y qué controles deben aplicarse en cada capa.

Nota: los ejemplos se muestran con fines educativos y defensivos. Las pruebas deben realizarse únicamente sobre aplicaciones propias o con autorización expresa.

En esta guía


Por qué un deep link es una frontera de seguridad

Un deep link es una URI que dirige al usuario a un contexto específico dentro de una aplicación. Puede contener una ruta, parámetros de consulta y un fragmento, igual que una URL web:

Esquema personalizado
sixhack://course/wxe?section=oauth

Enlace web verificado
https://sixhackacademy.example/course/wxe?section=oauth

Desde el punto de vista de la app, ambos pueden terminar llamando a una función interna que decide qué pantalla abrir y qué datos utilizar. La diferencia está en cómo determina el sistema operativo qué aplicación debe recibir el enlace.

El error habitual consiste en tratar el deep link como una navegación interna confiable. Pero el enlace puede proceder de un navegador, un correo, un código QR, otra app o una aplicación maliciosa instalada en el dispositivo. Por eso sus parámetros deben considerarse entrada externa, incluso cuando el dominio esté verificado.

Además, abrir la app no demuestra la identidad de quien originó la solicitud. Un deep link no es una sesión, una firma ni una autorización. Solo es un mecanismo de transporte y enrutamiento.

La llegada de un enlace nunca debe utilizarse, por sí sola, como prueba de identidad o permiso para ejecutar una acción sensible.

Tres modelos de enlace con garantías diferentes

TipoEjemploAsociación con el propietarioRiesgo principal
Esquema URI personalizadomiapp://perfilNo existe una verificación criptográfica o de dominio.Otra app puede declarar el mismo esquema.
Deep link web no verificadohttps://app.example/perfilLa app declara que puede manejarlo, pero la asociación puede no estar verificada.Desambiguación, apertura en navegador o captura por otro manejador.
App Link / Universal Linkhttps://app.example/perfilEl dominio autoriza expresamente a la app mediante un fichero publicado por HTTPS.Configuración incorrecta o lógica vulnerable en el manejador.

Los esquemas personalizados siguen siendo útiles para integraciones cerradas o navegación interna, pero no son nombres exclusivos. Usar una cadena con formato de dominio inverso, como com.example.app:/oauth2redirect, reduce colisiones accidentales, pero no impide que una app hostil registre exactamente el mismo valor.

En iOS, si varias apps registran el mismo esquema, Apple indica que la aplicación seleccionada por el sistema queda indefinida. En Android, varias apps pueden declarar filtros compatibles y el resultado depende de la versión, de las preferencias del usuario y de si existe una asociación verificada. En cualquiera de los dos casos, un esquema personalizado no aporta una garantía sólida de propiedad.


Cómo se produce el secuestro de un esquema personalizado

El ataque comienza con una colisión. La app legítima declara que puede recibir una URI y una app maliciosa instalada en el mismo dispositivo declara exactamente lo mismo.

# La aplicación legítima espera recibir
legitapp://callback

# La aplicación maliciosa registra el mismo esquema
legitapp://callback

A partir de ese momento, el sistema tiene más de un candidato. Según la plataforma y la configuración, puede mostrar un selector, utilizar una preferencia anterior o entregar la URI a uno de los manejadores registrados. La app maliciosa no necesita romper TLS ni controlar la red: se sitúa en el último tramo, cuando el sistema operativo convierte la redirección en una llamada entre aplicaciones.

La gravedad depende de lo que viaje dentro del enlace. Si solo contiene una ruta de navegación, el resultado puede limitarse a una mala experiencia. Si incluye un código OAuth, un token de recuperación, una invitación privilegiada o un identificador que activa una operación, la colisión se convierte en un problema de seguridad.

También existe una variante menos visible: la app legítima recibe correctamente el enlace, pero procesa sus parámetros de forma insegura. En ese caso no hay secuestro del manejador, sino abuso del propio punto de entrada.


El caso crítico: interceptar una respuesta OAuth

Las aplicaciones nativas suelen iniciar OAuth en un navegador externo. Después de que el usuario se autentique, el servidor de autorización necesita devolver la respuesta a la app. Una opción histórica consiste en utilizar un esquema personalizado como redirect_uri:

com.example.app:/oauth2redirect/provider

El flujo vulnerable puede representarse así:

# Intercepción conceptual de un authorization code
Paso 1  La app legítima abre el navegador con la solicitud OAuth
Paso 2  El usuario se autentica en el proveedor real
Paso 3  El proveedor redirige a legitapp://callback?code=AUTH_CODE
Paso 4  Una app maliciosa registrada para ese esquema recibe la URI
Paso 5  La app maliciosa intenta canjear el código en el token endpoint

La víctima puede haber iniciado sesión en el dominio correcto y haber visto una pantalla completamente legítima. El problema aparece después, en el canal utilizado para devolver el código a la app.

Por qué PKCE cambia el resultado

PKCE vincula cada solicitud de autorización con un secreto temporal generado por la instancia legítima de la aplicación. Antes de abrir el navegador, la app crea un code_verifier aleatorio y envía al servidor su derivación, el code_challenge. Cuando recibe el código, debe presentar también el verificador original.

# Solicitud de autorización
code_challenge = BASE64URL(SHA256(code_verifier))
code_challenge_method = S256

# Canje del código
authorization_code + code_verifier

Una app maliciosa que intercepte únicamente el código no conoce el code_verifier. El servidor calcula de nuevo el challenge y rechaza el canje si no coincide. Por eso PKCE con S256 es un control esencial en clientes nativos.

También hay que evitar una conclusión equivocada: el parámetro state sigue siendo importante para correlacionar la respuesta y prevenir ataques contra la transacción, pero no sustituye a PKCE frente a la interceptación del authorization code. Del mismo modo, un secreto incluido dentro de una app nativa no debe considerarse confidencial, porque puede extraerse del binario o compartirse entre todas las instalaciones.

Qué redirect URI debería utilizarse

Cuando la plataforma lo permite, los enlaces HTTPS reclamados y verificados son preferibles porque la asociación con el dominio impide que otra app se atribuya legítimamente esa URL. Si se utiliza un esquema personalizado, debe ser específico de la aplicación, seguir un formato de dominio inverso y estar siempre acompañado por PKCE.

El servidor de autorización debe registrar y comparar las redirect URI de forma estricta. Los patrones demasiado amplios y los open redirects en dominios permitidos pueden desviar códigos hacia ubicaciones controladas por un atacante incluso aunque el esquema no sea secuestrado.

La defensa de OAuth no depende de una sola medida: navegador externo, authorization code flow, PKCE con S256, redirect URI registrada de forma exacta y validación correcta de la transacción trabajan juntas.

Más allá de OAuth: otros patrones de abuso

Recuperación de contraseña, magic links e invitaciones

Un enlace de recuperación o de acceso sin contraseña suele contener un token de un solo uso. Si ese token viaja por un esquema que puede ser interceptado, otra aplicación puede recibirlo antes que la app legítima. La defensa consiste en utilizar enlaces HTTPS verificados, tokens de vida corta, uso único y validación en servidor.

Las invitaciones a organizaciones o espacios privilegiados también requieren cuidado. Abrir el enlace puede mostrar la invitación, pero aceptarla debería exigir una sesión válida y una confirmación vinculada al usuario correcto.

Acciones sensibles disparadas directamente

Un deep link como miapp://transfer?to=...&amount=... no debería ejecutar una transferencia solo porque la URI coincide con una ruta reconocida. El patrón seguro es abrir una pantalla de confirmación, recuperar el estado actual desde el servidor y volver a comprobar autenticación, autorización y reglas de negocio.

WebViews controlados por parámetros

Si un parámetro decide qué URL carga un WebView, el deep link puede convertirse en una navegación arbitraria:

miapp://web?url=https://attacker.example

Validar la URL mediante contains, startsWith o comparaciones sobre la cadena completa suele producir bypasses. La app debe analizar la URI, exigir el esquema correcto y comparar el host normalizado con una allowlist exacta. También debe evaluar rutas, puertos, redirecciones y los esquemas especiales que el WebView pueda interpretar.

Redirección de intents en Android

Una actividad exportada puede recibir un intent o una URI y, a continuación, lanzar otro intent controlado parcial o totalmente por el atacante. Esto puede convertir un componente público en puente hacia funcionalidades internas, servicios o proveedores de contenido que no estaban exportados.

Android 16 añade endurecimiento por defecto contra determinadas redirecciones de intents, pero no elimina la necesidad de validar el destino, limpiar flags de permisos y evitar el reenvío de intents anidados no confiables.

Inyecciones y manipulación de rutas

Los parámetros pueden terminar en consultas, búsquedas, plantillas, rutas de fichero o identificadores de objetos. El deep link solo es el punto de entrada; el impacto final depende del destino al que llegue el dato.

Destino del parámetroRiesgo
WebView o navegador internoPhishing, navegación arbitraria, XSS o abuso de interfaces JavaScript.
Ruta de ficheroPath traversal, lectura o sobrescritura.
Consulta o comandoInyección si no existe parametrización o validación.
Identificador de objetoIDOR/BOLA si no se verifica la propiedad.
Intent anidadoAcceso indirecto a componentes o permisos internos.

Android App Links en profundidad

Android App Links utiliza URLs http o https y establece una asociación entre el dominio y la aplicación. La app solicita la verificación en el manifiesto y el dominio publica un archivo Digital Asset Links.

Declaración en 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" />

    <data
        android:scheme="https"
        android:host="app.example"
        android:pathPrefix="/mobile/" />
</intent-filter>

El atributo android:autoVerify="true" solicita al sistema que compruebe la asociación. Pero su presencia no demuestra que la verificación haya tenido éxito.

Archivo assetlinks.json

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": ["AA:BB:CC:DD:EE:FF:..."]
    }
  }
]

Debe publicarse exactamente en:

https://app.example/.well-known/assetlinks.json

La documentación oficial exige HTTPS, contenido JSON válido y acceso sin redirecciones. Cada host declarado debe servir su propia asociación. Si se utiliza Play App Signing, la huella relevante puede ser la del certificado gestionado por la tienda y no la obtenida del keystore local.

Errores frecuentes

  • Publicar el fichero en www.example cuando el manifiesto declara example, o al contrario.
  • Responder con un 301 o 302 antes de llegar a assetlinks.json.
  • Utilizar la huella del certificado de debug o del certificado local equivocado.
  • Mezclar esquemas, hosts y rutas en un mismo filtro sin considerar que los elementos <data> pueden combinarse entre sí.
  • Declarar rutas demasiado amplias y enviar más URLs de las necesarias al manejador móvil.
  • Suponer que autoVerify equivale a estado verified.

Cambios recientes de Android

Desde Android 12, los web intents genéricos se dirigen normalmente al navegador salvo que la aplicación esté aprobada para el dominio o el usuario haya configurado una preferencia. Esto reduce parte de la superficie histórica de secuestro, pero no protege los esquemas personalizados.

Android 15 incorpora Dynamic App Links. Las reglas del archivo assetlinks.json pueden refinar rutas, parámetros, fragmentos y exclusiones sin publicar una nueva versión de la app. Esta flexibilidad también convierte el fichero del servidor en una pieza operativa más importante: un cambio incorrecto puede ampliar qué enlaces llegan a la aplicación.


iOS Universal Links en profundidad

Los Universal Links utilizan URLs HTTPS y una asociación bidireccional entre la aplicación y el dominio. La app declara el dominio mediante el entitlement de Associated Domains y el servidor publica el fichero apple-app-site-association, conocido como AASA.

Associated Domains Entitlement

applinks:app.example

El valor debe contener el dominio, sin ruta, parámetros ni barra final.

Archivo AASA

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.example.app"],
        "components": [
          { "/": "/mobile/*" },
          { "/": "/mobile/admin/*", "exclude": true }
        ]
      }
    ]
  }
}

El fichero se publica sin extensión en:

https://app.example/.well-known/apple-app-site-association

Debe servirse por HTTPS, con certificado válido y sin redirecciones. Cada subdominio utilizado necesita su entrada correspondiente y su propia asociación. En versiones modernas de iOS, el sistema obtiene el AASA mediante un CDN gestionado por Apple, lo que introduce caché y hace que algunos cambios no sean inmediatos.

El enlace verificado también debe validarse

Apple advierte expresamente que los Universal Links siguen siendo un vector de entrada. La app debe analizar la URL con APIs de componentes, descartar formatos malformados y limitar las operaciones disponibles desde el enlace.

Un dominio verificado responde a la pregunta “¿qué app puede recibir esta URL?”. No responde a “¿qué puede hacer esta URL dentro de la app?”. Esa segunda decisión sigue perteneciendo al manejador.


Por qué los enlaces verificados no son inmunes

App Links y Universal Links eliminan la colisión de esquemas para el dominio verificado, pero no solucionan automáticamente el resto del modelo de amenazas.

  • Parámetros manipulados: el atacante puede construir una URL válida con valores inesperados.
  • Reglas demasiado amplias: un patrón que captura todo el dominio envía al manejador rutas que nunca se diseñaron para la app.
  • Open redirects: una ruta de confianza que redirige libremente puede utilizarse en cadenas OAuth, phishing o navegación hacia contenido externo.
  • Dominios abandonados o comprometidos: la asociación sigue confiando en una propiedad web que quizá ya no controla la organización.
  • Acciones sin confirmación: la verificación del dominio no autoriza una transferencia, un borrado o una aceptación de invitación.
  • Autorización débil: un enlace a /invoice/123 sigue necesitando comprobar que la factura pertenece al usuario.
  • Fallback inseguro: si la asociación falla, una implementación puede volver a un esquema personalizado vulnerable.
“Verificado” describe el enrutamiento entre un dominio y una app. No significa que la lógica ejecutada después sea segura.

Metodología de auditoría

1. Construye un inventario

En Android, revisa los <intent-filter> del manifiesto, las actividades exportadas y las combinaciones resultantes de los elementos <data>. En iOS, revisa CFBundleURLTypes, Associated Domains, entitlements y el contenido del AASA.

Clasifica cada entrada como esquema personalizado, web link no verificado, App Link o Universal Link verificado, redirect URI de OAuth o enlace que activa una acción sensible.

2. Localiza el manejador

Sigue el dato desde la URI hasta la función que lo procesa. En Android, busca llamadas a getIntent(), getData(), getQueryParameter() y navegación posterior. En iOS, revisa los delegados que reciben URLs personalizadas o actividades de navegación web.

3. Dispara enlaces controlados

En Android puede utilizarse ADB:

# Invocar un esquema personalizado
adb shell am start -W \
  -a android.intent.action.VIEW \
  -d "demoapp://profile?id=123"

# Revisar asociaciones de App Links en Android 12+
adb shell pm get-app-links com.example.app

# Obtener información del paquete y sus filtros
adb shell dumpsys package com.example.app

En el simulador de iOS:

# Esquema personalizado
xcrun simctl openurl booted "demoapp://profile?id=123"

# Enlace HTTPS
xcrun simctl openurl booted "https://app.example/mobile/profile?id=123"

La activación del Universal Link debe comprobarse también en condiciones reales, porque el comportamiento depende de la asociación instalada y de las decisiones previas del sistema.

4. Verifica la asociación del dominio

No te limites al manifiesto o al entitlement. Comprueba el fichero remoto, la ruta exacta, el certificado, las redirecciones, los hosts, las huellas y el estado de verificación observado en el dispositivo.

5. Ataca los parámetros

Prueba valores ausentes, repetidos, codificados, muy largos, con Unicode, rutas alternativas y combinaciones que cambien la interpretación del parser. Si existe una URL anidada, valida esquemas, hosts, puertos, fragmentos y redirecciones.

https://app.example/mobile/web?url=https://attacker.example
https://app.example/mobile/invoice?id=OTHER_USER_ID
https://app.example/mobile/action?next=https://attacker.example

6. Revisa OAuth como una cadena completa

  • ¿La autorización se abre en un navegador externo?
  • ¿Se utiliza authorization code flow?
  • ¿PKCE es obligatorio y utiliza S256?
  • ¿El code_verifier es distinto para cada transacción?
  • ¿La redirect URI está registrada y comparada exactamente?
  • ¿Se validan state y, cuando corresponde, nonce?
  • ¿El código puede reutilizarse o canjearse desde otra instancia?

7. Prueba colisiones de forma controlada

En una aplicación de laboratorio puede registrarse el mismo esquema para demostrar que no existe propiedad exclusiva. La prueba de concepto debería limitarse a mostrar qué app recibe la URI y qué parámetros quedan expuestos, sin acceder a cuentas o datos de terceros.

8. Evalúa el impacto real

No todos los deep links secuestrables tienen la misma severidad. El informe debe explicar qué dato se intercepta, qué acción puede ejecutarse, qué interacción requiere la víctima y qué controles adicionales impiden o permiten completar la cadena.


Cómo se defiende correctamente

CapaControl
EnrutamientoUsar App Links o Universal Links para enlaces externos sensibles.
OAuthAuthorization code flow, PKCE con S256, navegador externo y redirect URI exacta.
EntradaAnalizar la URI y validar esquema, host, puerto, ruta y parámetros mediante allowlists.
AutorizaciónVolver a comprobar identidad, rol y propiedad del objeto en backend.
Acciones sensiblesExigir confirmación y obtener el estado actual desde el servidor.
DatosNo transportar sesiones, credenciales o información personal innecesaria en la URL.
PlataformaEvitar intent redirection, limpiar flags y limitar componentes exportados.
OperaciónMonitorizar dominios, certificados y ficheros de asociación durante todo su ciclo de vida.

Para enlaces externos, los esquemas personalizados deberían ser la excepción. Cuando no puedan evitarse, utiliza nombres específicos de la app, no transportes datos sensibles y asume siempre que otra aplicación puede registrar el mismo esquema.

La validación de hosts debe hacerse sobre componentes parseados, no mediante búsquedas de texto. Una comprobación como host.contains("example.com") puede aceptar valores como example.com.attacker.test. La comparación debe realizarse contra hosts exactos o subdominios permitidos con límites correctamente definidos.

Los tokens de recuperación, verificación e invitación deben ser de un solo uso, expirar pronto y estar vinculados al propósito para el que fueron emitidos. Interceptar el enlace no debería conceder automáticamente una sesión duradera.

Finalmente, la asociación del dominio necesita pruebas continuas. Un cambio de certificado, firma, subdominio, CDN o configuración del servidor puede hacer que un enlace verificado deje de estarlo sin modificar el código de la app.


Qué significa para seguridad ofensiva

Los deep links conectan varias áreas que a menudo se estudian por separado: OAuth, autorización web, intents de Android, entitlements de iOS, WebViews, navegación y configuración de dominios.

Por eso son especialmente interesantes en una auditoría. Un analista que solo revisa el manifiesto puede detectar una colisión, pero necesita comprender OAuth para saber si el código interceptado tiene valor. Quien solo revisa el backend puede no ver que una app hostil controla el último salto de la redirección. El impacto aparece al unir ambas capas.

Es el tipo de razonamiento que se trabaja en el curso Mobile eXploitation Specialist (MXS), donde se auditan aplicaciones Android e iOS, sus mecanismos de interacción con la plataforma, WebViews y componentes expuestos. La parte de OAuth, PKCE y lógica de autorización se complementa con el curso Web eXploitation Expert (WXE).

¿Quieres aprender a auditar aplicaciones móviles más allá del análisis superficial?

En SixHack Academy, el curso Mobile eXploitation Specialist (MXS) desarrolla una metodología completa para analizar aplicaciones Android e iOS, incluyendo deep links, Universal Links, App Links, WebViews e interacción entre componentes.


Preguntas frecuentes

¿Por qué un esquema personalizado puede ser secuestrado?

Porque su registro no demuestra propiedad. Otra aplicación puede declarar el mismo esquema. En iOS, el destino queda indefinido cuando varias apps lo registran; en Android pueden intervenir la desambiguación, las preferencias y la versión del sistema.

¿Robar un authorization code siempre permite tomar la cuenta?

No. El atacante necesita poder canjearlo. PKCE correctamente implementado vincula el código al code_verifier de la instancia legítima y hace inútil el código interceptado por sí solo.

¿El parámetro state sustituye a PKCE?

No. Ambos protegen aspectos relacionados pero diferentes de la transacción. Para clientes nativos, PKCE debe utilizarse y el estado de la solicitud también debe validarse correctamente.

¿App Links y Universal Links eliminan todos los riesgos?

No. Evitan que otra app reclame legítimamente el enlace del dominio verificado, pero los parámetros siguen siendo entrada no confiable y la aplicación debe aplicar autenticación, autorización y validación.

¿Es inseguro incluir identificadores en assetlinks.json o AASA?

No. Esos archivos son públicos por diseño. Su función es declarar la asociación. Lo sensible es proteger las claves privadas de firma y mantener el control del dominio y de la configuración publicada.

¿Qué debe probarse después de una actualización de firma o dominio?

La accesibilidad de los ficheros de asociación, las huellas o identificadores, el estado de verificación en dispositivos reales, el fallback y cada ruta sensible que dependa del enlace.

¿Puede un deep link abrir directamente una acción sensible?

Puede abrir la pantalla correspondiente, pero la operación debería requerir una sesión válida, autorización en backend y confirmación cuando el impacto lo justifique.


Referencias

← Volver a Artículos