5 cosas que todo profesional de la seguridad móvil debería saber sobre los WebViews
Este artículo trata sobre los WebViews y las nociones de seguridad que debemos tener presentes al usar estos componentes tanto en Android como en iOS.
WebView es un componente importante de las aplicaciones móviles: permite que las aplicaciones Android e iOS muestren contenido web y ejecuten código Javascript dentro de una aplicación móvil.
El diseño de WebView implica cambios en el panorama de la Web y exige una atención especial para evitar brechas de seguridad en las aplicaciones.
A través de 5 consejos breves y útiles, comprenderá el uso de los WebViews y los problemas de seguridad habituales que debe revisar.
Ostorlab ya comprueba automáticamente todos estos problemas mediante análisis estático y dinámico.
Dígame su versión del sistema operativo y le diré su WebView
Webview se introdujo en iOS 1 y Android 2, y ha experimentado cambios importantes para optimizar su rendimiento y mejorar su seguridad.
Estos son los principales hitos en el diseño de Webview:
-
iOS:
UIWebviewestá disponible desdeiOS1 y quedó obsoleto eniOS8. Tiene muchos problemas de seguridad:- NO se puede desactivar Javascript
- NO se puede desactivar el acceso a archivos
- NO se puede aplicar la política del mismo origen al acceso a archivos
- La aplicación nativa tiene acceso a todas las solicitudes y respuestas, lo que no es ideal para datos sensibles ni para la autenticación externa
- El contenido renderizado y la aplicación nativa comparten el mismo proceso
WKWebViewestá disponible a partir deiOS8 e introdujo varias mejoras de rendimiento y seguridad:- Se puede desactivar Javascript
- Se puede desactivar el acceso a archivos
- Se puede aplicar la política del mismo origen al acceso a archivos
- La aplicación nativa tiene acceso a todas las solicitudes y respuestas, lo que no es ideal para datos sensibles ni para la autenticación externa
- El contenido renderizado y la aplicación nativa se ejecutan en procesos distintos
SFSafariViewControllerestá disponible a partir deiOS9 y ofrece una experiencia similar a la de un navegador; se utiliza principalmente cuando la aplicación solo necesita mostrar contenido web.- Se puede desactivar Javascript
- Se puede desactivar el acceso a archivos
- Se puede aplicar la política del mismo origen al acceso a archivos
- La aplicación nativa NO puede acceder a todas las solicitudes y respuestas, pero se pueden usar distintas implementaciones para compartir las cookies y los datos almacenados
- El contenido renderizado y la aplicación nativa se ejecutan en procesos distintos.
-
Android:
WebKitde 2.x a 3:- Se puede desactivar Javascript
- No se puede desactivar el acceso a archivos
- NO se puede aplicar la política del mismo origen al acceso a archivos
- El contenido renderizado y la aplicación nativa comparten el mismo proceso
WebKiten 3:- Se puede desactivar Javascript
- Se puede desactivar el acceso a archivos
- NO se puede aplicar la política del mismo origen al acceso a archivos
- El contenido renderizado y la aplicación nativa comparten el mismo proceso
Chromium30 en 4.4:Webviewse ejecuta en el hilo de la interfaz de usuario- Se puede desactivar Javascript
- Se puede desactivar el acceso a archivos
- Se puede aplicar la política del mismo origen al acceso a archivos
- El contenido renderizado y la aplicación nativa comparten el mismo proceso
- Chromium M37 en 5.0:
- Se introdujo la clase
PermissionRequestpara conceder alWebViewpermiso de acceso a recursos protegidos como la cámara y el micrófono. - Admite las actualizaciones de Chromium desde Google Play Store
- Se introdujo la clase
- Desde Android 7.0 hasta Android :
- la API de geolocalización solo se permitirá en orígenes seguros (mediante HTTPS).
WebViewejecuta el contenido web en un proceso independiente con sandbox- Se añadió la API Safe Browsing para avisar cuando
WebViewintenta navegar a una URL que Google ha clasificado como una amenaza conocida.
¿Es seguro habilitar la depuración del contenido de los WebViews en producción?
La respuesta corta es: ¡NO!
En esta sección veremos cómo una aplicación maliciosa puede acceder a los datos que se muestran en un WebView si WebContentsDebugging está habilitado.
Para ilustrar cómo una aplicación habilita la depuración del contenido de los WebViews, utilizaré el entorno de análisis de Ostorlab y buscaré la función setWebContentsDebuggingEnabled.

Para comprobar el árbol de llamadas y los parámetros que se pasan a la función, voy a la pestaña del árbol de llamadas:

Podemos ver que initWebView llama a setWebContentsDebuggingEnabled:

Al saltar al código fuente, vemos que el parámetro se obtiene del método this.config.isWebContentsDebuggingEnabled()
private void initWebView()
{
android.webkit.WebSettings v0_1 = this.webView.getSettings();
v0_1.setJavaScriptEnabled(1);
v0_1.setDomStorageEnabled(1);
v0_1.setGeolocationEnabled(1);
v0_1.setDatabaseEnabled(1);
v0_1.setAppCacheEnabled(1);
v0_1.setMediaPlaybackRequiresUserGesture(0);
v0_1.setJavaScriptCanOpenWindowsAutomatically(1);
if (this.config.isMixedContentAllowed()) {
v0_1.setMixedContentMode(0);
}
String v1_3 = this.config.getAppendedUserAgentString();
if (v1_3 != null) {
String v2_0 = v0_1.getUserAgentString();
String v3_1 = new StringBuilder();
v3_1.append(v2_0);
v3_1.append( );
v3_1.append(v1_3);
v0_1.setUserAgentString(v3_1.toString());
}
String v2_2 = this.config.getOverriddenUserAgentString();
if (v2_2 != null) {
v0_1.setUserAgentString(v2_2);
}
String v3_4 = this.config.getBackgroundColor();
if (v3_4 == null) {
} else {
try {
this.webView.setBackgroundColor(com.getcapacitor.util.WebColor.parseColor(v3_4));
} catch (boolean v4) {
com.getcapacitor.Logger.debug(WebView background color not applied);
}
}
this.webView.requestFocusFromTouch();
android.webkit.WebView.setWebContentsDebuggingEnabled(this.config.isWebContentsDebuggingEnabled());
return;
}
Al comprobar la definición de isWebContentsDebuggingEnabled, vemos que utiliza capacitor.config.json y que el parámetro está establecido en true.

El siguiente paso es instalar la aplicación y ejecutarla en el teléfono.
La depuración de WebView utiliza el Chrome Debug Protocol y se expone mediante un socket unix abstracto con nombre. El socket se llama
webview_devtools_remote o webview_devtools_remote_<pid>.
Los sockets abstractos no utilizan los permisos del sistema de archivos para controlar el acceso y, por tanto, son accesibles para todas las aplicaciones del dispositivo.
Podemos usar netstat para encontrar el socket expuesto:
shell# netstat -untapexW | grep webview_devtools_remote
unix 2 [ ACC ] STREAM LISTENING 2633690 26634/com.xxxxx.i@webview_devtools_remote_26634
Ahora, para explotarlo y leer el contenido del socket, solo tenemos que ejecutar el siguiente comando en el teléfono:
socat TCP-LISTEN:9999,fork ABSTRACT:webview_devtools_remote_2466
Tenga en cuenta que Java no dispone de una API para acceder a sockets abstractos, por lo que los atacantes que realicen este tipo de ataque probablemente usarán código nativo.
Para acceder al protocolo remoto, utilice un cliente del Chrome Debug Protocol, como pychrome:
import pychrome
# connect to webview on the exposed port.
browser = pychrome.Browser(url="http://127.0.0.1:9999")
t = browser.list_tab()[0]
t.start()
t.DOM.enable()
# Access document.
t.DOM.getDocument()
Podemos usar el objeto DOM para inspeccionar todo el contenido de la página:
>>> t.DOM.getDocument()
{'root': {'nodeId': 1, 'backendNodeId': 2, 'nodeType': 9, 'nodeName': '#document', 'localName': '', 'nodeValue': '', 'childNodeCount': 2, 'children': [{'nodeId': 2, 'parentId': 1, 'backendNodeId': 42, 'nodeType': 10, 'nodeName': 'html', 'localName': '', 'nodeValue': '', 'publicId': '', 'systemId': ''}, {'nodeId': 3, 'parentId': 1, 'backendNodeId': 43, 'nodeType': 1, 'nodeName': 'HTML', 'localName': 'html', 'nodeValue': '', 'childNodeCount': 2, 'children': [{'nodeId': 4, 'parentId': 3, 'backendNodeId': 44, 'nodeType': 1, 'nodeName': 'HEAD', 'localName': 'head', 'nodeValue': '', 'childNodeCount': 80, 'attributes': []}, {'nodeId': 5, 'parentId': 3, 'backendNodeId': 45, 'nodeType': 1, 'nodeName': 'BODY', 'localName': 'body', 'nodeValue': '', 'childNodeCount': 5, 'attributes': []}], 'attributes': ['lang', 'en'], 'frameId': '8C9DD9891A40F2CEC8D73094D29D9152'}], 'documentURL': 'https://www.xxx.com/', 'baseURL': 'https://www.xxx.com/', 'xmlVersion': ''}}
Un viaje seguro del código Java a Javascript:
Los objetos Java pueden inyectarse en el WebView y exponerse a JavaScript mediante el método addJavascriptInterface. A continuación se muestra un ejemplo sencillo que ilustra cómo implementarlo:
webView = (WebView) findViewById(R.id.webView1);
webView.addJavascriptInterface(new JavaScriptBridge(), "safeBridge");
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebChromeClient(new WebChromeClient());
webView.loadUrl("file:///android_asset/main.html");
public class JavaScriptBridge {
@JavascriptInterface
public String helloSafeWorld()
{
return "Hello World!";
}
}
En este ejemplo, el método helloSafeWorld() puede invocarse desde JavaScript con el siguiente código:
var HelloWorld = window.safeBridge.helloSafeWorld();
Una vez que una interfaz se registra en el WebView mediante addJavascriptInterface, pasa a ser global. Todas las páginas cargadas en
el WebView pueden llamar a esta interfaz y acceder a los mismos datos que mantiene la interfaz. Esto hace posible que
las páginas web de un origen afecten a las de otros
A partir de la versión 17 de la API, solo los métodos con la anotación @JavascriptInterface están disponibles para el código JavaScript.
Antes de la versión 17 de la API, podía usarse la reflexión para ejecutar código arbitrario en el dispositivo (véase CVE-2012-6636).
Veamos un ejemplo real de la aplicación com.microsoft.skydrive. Primero buscamos la función addJavascriptInterface

En la pestaña de la pila de llamadas, vemos que hay varias interfaces expuestas.

Nos centraremos en el paquete com.microsoft.skydrive.reportabuse.

public void onViewCreated(android.view.View p3, android.os.Bundle p4)
{
kotlin.jvm.internal.Intrinsics.checkNotNullParameter(p3, view);
android.webkit.WebView v3_12 = ((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view));
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v3_12, web_view);
android.webkit.WebView v3_13 = v3_12.getSettings();
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v3_13, web_view.settings);
v3_13.setJavaScriptEnabled(1);
((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view)).addJavascriptInterface(new com.microsoft.skydrive.reportabuse.ReportAbuseJavascriptInterface(this), external);
android.webkit.WebView v3_7 = ((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view));
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v3_7, web_view);
v3_7.setWebViewClient(new com.microsoft.skydrive.reportabuse.ReportAbuseDialogFragment$onViewCreated$1(this));
((android.webkit.WebView) this._$_findCachedViewById(com.microsoft.skydrive.R$id.web_view)).loadUrl(https://www.onedrive.com/reportabuse);
return;
}
La interfaz expone los siguientes métodos:
package com.microsoft.skydrive.reportabuse;
public interface ReportAbuseInterface {
public abstract void dismissReportAbuse();
public abstract String getReportAbuseContextInformation();
public abstract void pageFinishedLoading();
public abstract void reportClicked();
public abstract void resize();
}
Un elemento clave al implementar estos métodos es validar cada entrada y evitar crear comportamientos genéricos que un atacante pueda utilizar para recuperar o modificar datos sensibles.
En la implementación de reportClicked que se muestra a continuación, llamar a la función con un valor null provocará un error al llamar a valueOf, lo que da lugar a un comportamiento inesperado.
public void reportClicked(String p12, String p13)
{
android.content.Context v1_1 = this.getContext();
if (v1_1 != null) {
com.microsoft.authorization.instrumentation.AccountInstrumentationEvent v9_1 = new com.microsoft.authorization.instrumentation.AccountInstrumentationEvent(v1_1, com.microsoft.skydrive.instrumentation.EventMetaDataIDs.REPORT_ABUSE_CLICKED, this.a);
try {
com.microsoft.skydrive.reportabuse.ReportAbuseTask v2_0 = com.microsoft.skydrive.reportabuse.ReportAbuseDialogFragment$ReportAbuseType.valueOf(p12);
com.microsoft.authorization.OneDriveAccount v4 = this.a;
} catch (IllegalArgumentException) {
String v13_2 = new StringBuilder();
v13_2.append(Invalid report abuse type - );
v13_2.append(p12);
com.microsoft.odsp.io.Log.dPiiFree(ReportAbuseDialogFragment, v13_2.toString());
kotlin.jvm.internal.Intrinsics.checkNotNullExpressionValue(v1_1, context);
this.b(v1_1, 2131952415);
v9_1.addProperty(InvalidReportAbuseType, p12);
com.microsoft.instrumentation.util.ClientAnalyticsSession.getInstance().logEvent(v9_1);
}
...
Probemos con iOS: seguramente sea más seguro
Antes de iOS 7, implementar un puente nativo en iOS es algo más complejo que en Android: no hay métodos de API explícitos definidos para este fin.
La forma habitual de hacerlo consistía en sobrecargar el sistema de carga de URL para que se pudieran pasar mensajes arbitrarios desde JavaScript a un callback en el UIWebView nativo.
Cada vez que se carga una URL dentro del WebView, se invoca el método delegado shouldStartLoadWithRequest, que intercepta la URL completa, incluidos los parámetros.
El formato de la URL se utiliza normalmente para pasar mensajes desde JavaScript al contenedor nativo.
Por ejemplo, se puede usar lo siguiente para buscar un nombre en la lista de empleados:
window.location = mysafebridge://employees/search/contact?firstname=john
A continuación, el contenedor nativo implementa el delegado shouldStartLoadWithRequest del WebView con un código similar al siguiente:
- (BOOL)webView:(UIWebView*)webView
shouldStartLoadWithRequest:(NSURLRequest*)request
navigationType:(UIWebViewNavigationType)navigationType {
NSURL *URL = [request URL];
if ([[URL scheme] isEqualToString:@"mysafebridge"]) {
// parse URL, extract host and parameters to define actions
}
}
El método shouldStartLoadWithRequest normalmente leería la URL y luego separaría e interpretaría cada uno de sus componentes para determinar qué acciones debe realizar.
Sin embargo, la técnica de carga de URL solo ofrece un puente unidireccional desde la capa web hasta el contenedor nativo. Es
posible crear un canal de comunicación bidireccional mediante un callback de JavaScript y el método stringByEvaluatingJavaScriptFromString de la clase UIWebview.
Por ejemplo, para ejecutar un método de JavaScript desde el contenedor nativo podría encontrarse un código similar al siguiente:
[webView stringByEvaluatingJavaScriptFromString: @"addEmployee('%@','%@')",firstname,job];
Este sencillo ejemplo haría que se ejecutara la función JavaScript addEmployee(), pasando a JavaScript los objetos NSString
"firstname" y "job". Usada junto con shouldStartLoadWithRequest, esta técnica
es capaz de proporcionar un puente rudimentario entre las capas nativa y web.
Hay que tener cuidado al usar esquemas de URI personalizados, ya que un atacante puede compartir un enlace malicioso (por correo electrónico, chat o SMS) y código JavaScript o HTML malicioso invocará funcionalidades móviles nativas y explotará sus datos.
Nota: la ejecución de JavaScript en UIWebView limita el total de asignaciones de memoria a 10MB y el tiempo de ejecución a 10 segundos, momento en el que
la ejecución se detendrá de forma inmediata y tajante.
Para superar las limitaciones de UIWebView, iOS 7 incorporó el framework JavaScriptCore, que ofrece compatibilidad total
para el puente de comunicaciones entre Objective-C nativo y un entorno de ejecución de JavaScript. El puente se crea mediante el nuevo
objeto global JSContext, que proporciona acceso a una máquina virtual de JavaScript para evaluar código. El entorno de ejecución de
Objective-C también puede obtener referencias fuertes a valores de JavaScript mediante objetos JSValue.
El protocolo JSExport permite que las aplicaciones expongan clases e instancias completas de Objective-C a JavaScript y operen con ellas como si fueran objetos de JavaScript.
Definir variables y métodos dentro de un protocolo que hereda de JSExport indica a JavaScriptCore1 que se puede acceder a esos elementos desde JavaScript:
@objc public protocol CarJSExports : JSExport {
var model: String { get set }
var year: String { get set }
var price: NSNumber? { get set }
var fullDetail: String { get }
static func createWith(model: String, year: String) -> Car
}
En el ejemplo anterior, la declaración del protocolo JSExport permite que Javascript acceda a las variables model y year y a la función createWith.
Ahora que JavaScriptCore conoce el protocolo CarJSExports, puede crear un objeto contenedor adecuado cuando se añade una instancia de este a un JSContext:
@objc public class Car : NSObject, CarJSExports {
public dynamic var model: String
public dynamic var year: String
public dynamic var price: NSNumber?
public required init(model: String, year: String) {
self.model = model
self.year = year
}
public class func createWith(model: String, year: String) -> Car {
return Car(model: model, year: year)
}
public var fullDetail: String {
return "\(model) \(year)"
}
}
let context = JSContext()!
context.setObject(Car.self, forKeyedSubscript: "Car" as NSString)
context.evaluateScript(#"""
function loadCar(json) {
return JSON.parse(json)
.map((attributes) => {
let car = Car.createWithModelYear(attributes.model, attributes.year);
car.price = attributes.price;
return car;
});
}
"""#)
let json = """
[
{ "model": "Tesla", "year": "2020", "price": 999 },
{ "model": "Toyota", "year": "2220", "price": 998 },
{ "model": "Mercedes", "year": "2222", "price": 909 }
]
"""
guard let loadCar = context.objectForKeyedSubscript("loadCar"),
let cars = loadCar.call(withArguments: [json])?.toArray()
else {
fatalError()
}
for car in cars {
let model = (car as! Car).model
NSLog(model);
}
Con esta implementación, resulta fácil exponer objetos a JavaScriptCore; por eso los desarrolladores deben asegurarse
de exponer solo los datos necesarios y de no replicar la definición de JSExport en el modelo de datos original.
Por ejemplo, si la función fullDetail pudiera contener datos sensibles, no debería declararse en el protocolo CarJSExports y solo debería declararse en la definición de la clase Car.
¿Puedo leer un archivo más?
En la mayoría de los SDK y componentes WebViews, por defecto se permite cargar archivos del sistema de archivos. Esto supone un riesgo
cuando una aplicación maliciosa puede abrir archivos locales dentro del WebView de otra aplicación. Esto expone el WebView
a una multitud de técnicas de explotación, desde el abuso de la configuración de accesibilidad hasta la elusión del mismo origen.
En Android, puede desactivar el acceso al sistema de archivos desde un WebView de la siguiente manera:
webview.getSettings().setAllowFileAccess(false);
Esto no impedirá que el WebView pueda cargar archivos de la carpeta de recursos o de assets de la aplicación mediante
file:///android_res y file:///android_asset. Para blindar el WebView, no se debe permitir que los
archivos cargados desde el sistema de archivos accedan a otros archivos. Esto impedirá que la página cargada exfiltre
archivos privados.
webview.getSettings().setAllowFileAccessFromFileURLs(false);
webview.getSettings().setAllowUniversalAccessFromFileURLs(false);
Además, puede proteger un WebView para que no pueda acceder a los content providers del dispositivo mediante el siguiente ajuste:
webview.getSettings().setAllowContentAccess(false);
A continuación se muestra un ejemplo vulnerable en el que un atacante utiliza un enlace malicioso para leer datos sensibles del directorio de la aplicación. La aplicación escribe una contraseña en texto claro en las preferencias compartidas:
SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("password", "MyBadPassword");
editor.apply();
Y la aplicación utiliza un Webview para mostrar una URL procedente de un intent o de una entrada del usuario.
String badUrl = getIntent().getStringExtra("URL");
WebView webview = findViewById(R.id.webview);
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webview.setWebChromeClient(new WebChromeClient());
webview.loadUrl(badUrl);
En este caso, tanto JavaScript como AllowFileAccessFromFileURLs están habilitados. El archivo malicioso test.html lee el
archivo de preferencias compartidas MainActivity.xml y es capaz de exfiltrarlo.
function readTextFile(file)
{
var rawFile = new XMLHttpRequest();
rawFile.open("GET", file, false);
rawFile.onreadystatechange = function ()
{
if(rawFile.readyState === 4)
{
if(rawFile.status === 200 || rawFile.status == 0)
{
var allText = rawFile.responseText;
// send allText to external link
}
}
}
rawFile.send(null);.0
};
Al acceder a la URL dentro de la aplicación, vemos que se ejecuta el código malicioso y que el contenido del archivo es accesible:

Resumen
Webview es un componente importante de las aplicaciones móviles. Ofrece mucha flexibilidad para acceder a recursos externos e interactuar con ellos,
pero conlleva muchos desafíos de seguridad.
En resumen, asegúrese de usar la última versión del SDK y de evitar los componentes obsoletos, y tenga cuidado al habilitar el código JavaScript o al implementar puentes entre el código nativo y JavaScript. Las funcionalidades deben restringirse al mínimo necesario.
Espero que le haya resultado útil y no dude en probar su aplicación en Ostorlab.