La política de navegación por defecto de un WebView está mal en ambas direcciones. Si la dejas, cada enlace —incluida la página de pago del banco y el clon de phishing— se abre dentro de tu propio WebView, filtrando tu Referer, tu cookie de sesión y, en CVE reales ya divulgados, tu token de autenticación directamente en la barra de URL. Si la tocas, envías crashes ERR_UNKNOWN_URL_SCHEME la primera vez que un usuario toca un número de teléfono. Y como por defecto no hay lista blanca de host, una sola llamada de puente sin protección se convierte en una toma de cuenta silenciosa. Un runtime propio del host decide, por enlace y por origen, qué sale y qué se queda.
El comportamiento por defecto es «ábreme todo aquí»
Por defecto, un WebView carga internamente cada destino https. Un toque en la página de la tienda de un socio, una redirección bancaria o incluso una noticia externa se renderiza dentro de la propia interfaz de tu app. Es conveniente hasta que se vuelve un pasivo: el sitio de destino ahora recibe tu cabecera Referer, cualquier token que hayas añadido a la URL y el identificador persistente que tu WebView envió en su User-Agent. En 2026 se divulgó la app Mira para Android (CVE-2026-66832, CVSS 6.5) precisamente por esto: añadía el token de sesión en vivo del usuario a la URL como parámetro de consulta y ponía un ID de usuario estable en el User-Agent del WebView, transmitiendo ambos a propiedades de terceros, registros de Referer y cualquier script en el WebView. Eso es toma de cuenta, no una hipótesis.
Si lo arreglas mal, envías crashes
La reacción habitual es sobreescribir shouldOverrideUrlLoading y enrutar los esquemas no http al sistema. Si inviertes el booleano no pasa nada; si omites tel:, mailto:, sms:, geo: o intent://, el usuario ve ERR_UNKNOWN_URL_SCHEME — el WebView simplemente no puede renderizar un esquema que no entiende. En Android 7+ (API 24) la nueva firma es obligatoria y un valor de retorno incorrecto lanza ActivityNotFoundException y crashea. Android 11+ añade reglas de visibilidad de paquetes, así que un deep link a otra app falla en silencio salvo que se declare. La solución es real pero delicada, y vive en código nativo que el equipo web no posee.
Sin lista blanca, el puente es una puerta abierta
El fallo más profundo es estructural. La política de navegación y el puente JS son las dos guardas entre el contenido web y el poder nativo, y en una auditoría publicada (SilentGrid) ambas faltaban: el puente devolvía el token de autenticación en vivo a cualquier JavaScript de la página, y el decidePolicyFor del WKWebView permitía cualquier origen https sin comprobación de host. Un solo deep link construido bastó para cargar una página atacante, llamar al manejador de tokens y exfiltrar la sesión — en un solo toque, sin que la víctima viera nada. El tabnabbing inverso lo agrava: una página abierta sin rel="noopener" puede redirigir en silencio tu opener a un clon de phishing, y los WebView no aplican el valor por defecto del navegador.
La solución del runtime
Un runtime propio del host hace que el límite de navegación sea explícito y obligatorio. Mantiene una lista blanca de host: los dominios propios se cargan en el runtime, los esquemas de app conocidos (tel:, mailto:, market:) se enrutan al sistema con respuestas seguras, y todo lo demás se abre en un navegador externo o se bloquea. El puente JS se verifica por origen y con mínimos privilegios, para que una página de un catálogo que no se confía no alcance un manejador de credenciales. El enlace que debe salir, sale; el token que debe quedarse, se queda.
| Dimensión | WebView | Runtime propio del host |
|---|---|---|
| Comportamiento por defecto | Cada https se abre dentro del WebView | Los propios cargan dentro; el resto sale o se bloquea |
| Enlace externo | Referer, token en URL, ID de UA filtrados a terceros | Lista blanca de host gobierna cada origen |
| Esquemas personalizados | Crash ERR_UNKNOWN_URL_SCHEME si no se maneja | Enrutados al sistema con respuesta segura |
| Puente JS | A menudo sin verificación de origen | Verificado por origen, mínimos privilegios |
| Tabnabbing | noopener no se fuerza | Forzado en cada nueva ventana |
El hilo conductor de esta serie es el control. Un WebView entrega a tu límite de navegación un valor por defecto inseguro en ambas direcciones y un puente que confía en quien lo llame. Un runtime convierte el límite en una política que tú escribes — por enlace, por origen, por entrada de catálogo. Para un catálogo de páginas ajenas que transan dentro de tu app, esa es la diferencia entre un escaparate controlado y una puerta abierta.
Fuentes
- CVE-2026-66832 — la app Mira para Android filtra el token de sesión en la URL y el ID persistente en el UA del WebView a terceros (CVSS 6.5) — divulgado en 2026: material de autenticación e identificador de rastreo transmitidos más allá del límite de la app.
- Robo de tokens de autenticación a través del puente WebView — un puente JS sin verificación de origen más una política de navegación sin lista blanca encadena una toma de cuenta silenciosa — auditoría SilentGrid: un deep link, un toque, token exfiltrado.
- WebView de Android y App Links: el comportamiento por defecto lo carga todo internamente — App Links, tel:, mailto:, intent:// atrapados dentro del WebView salvo que se intercepten.
- ERR_UNKNOWN_URL_SCHEME: WebView solo renderiza http/https/file/data/about; cualquier otro esquema debe ser un Intent — la solución canónica y por qué el crash aparece solo en login, pago o compartir.
- Tabnabbing inverso en WebViews móviles — la referencia opener es abusable sin noopener y los WebView pueden no aplicar el valor por defecto del navegador — cómo una página abierta redirige en silencio tu opener al phishing.