5 choses que tout professionnel de la sécurité mobile doit savoir sur les WebViews
Cet article porte sur les WebViews et sur les notions de sécurité à garder à l'esprit lorsqu'on utilise ce composant sur Android comme sur iOS.
WebView est un composant important des applications mobiles : il permet aux applications Android et iOS d'afficher du contenu web et d'exécuter du code Javascript à l'intérieur d'une application mobile.
La conception de WebView modifie le paysage du Web et demande une attention particulière pour éviter des failles de sécurité dans les applications.
À travers 5 conseils concis et utiles, vous comprendrez l'utilisation des WebViews et les problèmes de sécurité courants à vérifier.
Ostorlab vérifie déjà automatiquement tous ces problèmes, par analyse statique comme par analyse dynamique.
Dites-moi votre version d'OS, je vous dirai votre webview ?
Webview a été introduit dans iOS 1 et Android 2, et a connu d'importantes évolutions pour optimiser ses performances et renforcer sa sécurité.
Voici les principales étapes de la conception de Webview :
-
iOS :
UIWebviewest disponible depuisiOS1 et obsolète depuisiOS8. Il présente de nombreux problèmes de sécurité :- Vous ne pouvez PAS désactiver Javascript
- Vous ne pouvez PAS désactiver l'accès aux fichiers
- Vous ne pouvez PAS appliquer la politique de même origine pour l'accès aux fichiers
- L'application native a accès à toutes les requêtes et réponses, ce qui n'est pas idéal pour les données sensibles et l'authentification externe
- Le contenu affiché et l'application native partagent le même processus
WKWebViewest disponible à partir d'iOS8 et a apporté de multiples améliorations de performance et de sécurité :- Vous pouvez désactiver Javascript
- Vous pouvez désactiver l'accès aux fichiers
- Vous pouvez appliquer la politique de même origine pour l'accès aux fichiers
- L'application native a accès à toutes les requêtes et réponses, ce qui n'est pas idéal pour les données sensibles et l'authentification externe
- Le contenu affiché et l'application native s'exécutent dans des processus différents
SFSafariViewControllerest disponible à partir d'iOS9 et offre une expérience proche de celle d'un navigateur ; il est principalement utilisé lorsque l'application n'a besoin que d'afficher du contenu web.- Vous pouvez désactiver Javascript
- Vous pouvez désactiver l'accès aux fichiers
- Vous pouvez appliquer la politique de même origine pour l'accès aux fichiers
- L'application native ne peut PAS accéder à toutes les requêtes et réponses, mais vous pouvez utiliser différentes implémentations pour partager les cookies et les données stockées
- Le contenu affiché et l'application native s'exécutent dans des processus différents.
-
Android :
WebKitde 2.x à 3 :- Vous pouvez désactiver Javascript
- Vous ne pouvez pas désactiver l'accès aux fichiers
- Vous ne pouvez PAS appliquer la politique de même origine pour l'accès aux fichiers
- Le contenu affiché et l'application native partagent le même processus
WebKitsur 3 :- Vous pouvez désactiver Javascript
- Vous pouvez désactiver l'accès aux fichiers
- Vous ne pouvez PAS appliquer la politique de même origine pour l'accès aux fichiers
- Le contenu affiché et l'application native partagent le même processus
Chromium30 sur 4.4 :Webviews'exécute dans le thread d'interface utilisateur- Vous pouvez désactiver Javascript
- Vous pouvez désactiver l'accès aux fichiers
- Vous pouvez appliquer la politique de même origine pour l'accès aux fichiers
- Le contenu affiché et l'application native partagent le même processus
- Chromium M37 sur 5.0 :
- Introduction de la classe
PermissionRequestpour accorder àWebViewl'autorisation d'accéder à des ressources protégées comme la caméra et le microphone. - Prise en charge des mises à jour de Chromium depuis le Google Play Store
- Introduction de la classe
- D'Android 7.0 à Android :
- l'API de géolocalisation n'est autorisée que sur des origines sécurisées (via HTTPS).
WebViewexécute le contenu web dans un processus distinct placé en sandbox- Ajout de l'API Safe Browsing pour avertir lorsque
WebViewtente de naviguer vers une URL que Google a classée comme menace connue.
Est-il sûr d'activer le débogage du contenu des WebViews en production ?
La réponse courte est : NON !
Dans cette section, nous allons voir comment une application malveillante peut accéder aux données affichées dans une WebView si WebContentsDebugging est activé.
Pour illustrer comment une application active le débogage du contenu des WebViews, je vais utiliser l'environnement d'analyse d'Ostorlab et rechercher la fonction setWebContentsDebuggingEnabled.

Pour consulter l'arbre d'appels et les paramètres passés à la fonction, je vais dans l'onglet de l'arbre d'appels :

Nous voyons que initWebView appelle setWebContentsDebuggingEnabled :

En passant au code source, nous voyons que le paramètre est récupéré depuis la méthode 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;
}
En vérifiant la définition de isWebContentsDebuggingEnabled, nous voyons qu'elle utilise capacitor.config.json et que le paramètre est défini sur true.

Ensuite, nous installons l'application et l'exécutons sur le téléphone.
Le débogage de WebView utilise le Chrome Debug Protocol, et il est exposé via un socket unix abstrait nommé. Le socket s'appelle soit
webview_devtools_remote, soit webview_devtools_remote_<pid>.
Les sockets abstraits n'utilisent pas les permissions du système de fichiers pour contrôler l'accès : ils sont donc accessibles à toutes les applications de l'appareil.
Nous pouvons utiliser netstat pour trouver le socket exposé :
shell# netstat -untapexW | grep webview_devtools_remote
unix 2 [ ACC ] STREAM LISTENING 2633690 26634/com.xxxxx.i@webview_devtools_remote_26634
Pour l'exploiter et lire le contenu du socket, il suffit d'exécuter la commande suivante sur le téléphone :
socat TCP-LISTEN:9999,fork ABSTRACT:webview_devtools_remote_2466
Notez que Java ne dispose pas d'API pour accéder à un socket abstrait ; les attaquants qui réalisent ce type d'attaque utiliseront probablement du code natif.
Pour accéder au protocole distant, utilisez un client du Chrome Debug Protocol, comme 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()
Nous pouvons utiliser l'objet DOM pour inspecter tout le contenu de la page :
>>> 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 voyage sûr du code Java vers Javascript :
Des objets Java peuvent être injectés dans la WebView et exposés à JavaScript au moyen de la méthode addJavascriptInterface. Voici un exemple simple qui montre comment l'implémenter :
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!";
}
}
Dans cet exemple, la méthode helloSafeWorld() peut être appelée depuis JavaScript avec le code suivant :
var HelloWorld = window.safeBridge.helloSafeWorld();
Une fois qu'une interface est enregistrée dans WebView via addJavascriptInterface, elle devient globale. Toutes les pages chargées dans
la WebView peuvent appeler cette interface et accéder aux mêmes données gérées par l'interface. Cela permet à
des pages web d'une origine d'affecter celles d'autres origines
À partir de la version 17 de l'API, seules les méthodes portant l'annotation @JavascriptInterface sont disponibles pour le code JavaScript.
Avant la version 17 de l'API, la réflexion pouvait être utilisée pour exécuter du code arbitraire sur l'appareil (voir CVE-2012-6636).
Examinons un exemple réel avec l'application com.microsoft.skydrive. Nous commençons par rechercher la fonction addJavascriptInterface

Dans l'onglet de la pile d'appels, nous voyons que plusieurs interfaces sont exposées.

Nous allons nous concentrer sur le paquet 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;
}
L'interface expose les méthodes suivantes :
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 point essentiel lors de l'implémentation de ces méthodes est de valider chaque entrée et d'éviter de créer des comportements génériques qu'un attaquant pourrait utiliser pour récupérer ou modifier des données sensibles.
Dans l'implémentation de reportClicked ci-dessous, appeler la fonction avec une valeur null déclenchera une erreur lors de l'appel à valueOf, ce qui conduit à un comportement inattendu.
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);
}
...
Essayons iOS, c'est probablement plus sûr !
Avant iOS 7, l'implémentation d'un pont natif sur iOS est un peu plus complexe que sur Android : aucune méthode d'API explicite n'est définie à cet effet.
La méthode courante consistait à détourner le système de chargement des URL afin de pouvoir transmettre des messages arbitraires depuis JavaScript vers un callback de l'UIWebView native.
Chaque fois qu'une URL est chargée dans la WebView, elle invoque la méthode déléguée shouldStartLoadWithRequest, qui intercepte l'URL complète, paramètres compris.
Le format de l'URL sert généralement à transmettre des messages de JavaScript vers le conteneur natif.
Par exemple, ce qui suit peut servir à rechercher un nom dans la liste des employés :
window.location = mysafebridge://employees/search/contact?firstname=john
Le conteneur natif implémente ensuite le délégué shouldStartLoadWithRequest de la WebView avec un code similaire à celui-ci :
- (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
}
}
La méthode shouldStartLoadWithRequest lit généralement l'URL, puis sépare et interprète chacun de ses composants pour déterminer les actions à entreprendre.
La technique de chargement d'URL ne fournit toutefois qu'un pont à sens unique, de la couche web vers le conteneur natif. Il est
possible de créer un canal de communication bidirectionnel à l'aide d'un callback JavaScript et de la méthode stringByEvaluatingJavaScriptFromString de la classe UIWebview.
Par exemple, pour exécuter une méthode JavaScript depuis le conteneur natif, vous pouvez trouver un code semblable à celui-ci :
[webView stringByEvaluatingJavaScriptFromString: @"addEmployee('%@','%@')",firstname,job];
Cet exemple simple provoquerait l'exécution de la fonction JavaScript addEmployee(), en lui passant les objets NSString
« firstname » et « job ». Utilisée conjointement avec shouldStartLoadWithRequest, cette technique
permet de fournir un pont rudimentaire entre les couches native et web.
Vous devez être prudent lorsque vous utilisez des schémas d'URI personnalisés, car un attaquant peut partager un lien malveillant (par e-mail, chat ou SMS), et du code JavaScript ou HTML malveillant invoquera alors des fonctionnalités natives du mobile et exploitera ses données.
Remarque : l'exécution JavaScript dans UIWebView limite le total des allocations à 10MB et la durée d'exécution à 10 secondes, après quoi
l'exécution est immédiatement et définitivement interrompue.
Pour dépasser les limites d'UIWebView, iOS 7 est livré avec le framework JavaScriptCore, qui prend entièrement en charge
les communications de pont entre Objective-C natif et un environnement d'exécution JavaScript. Le pont est créé via le nouvel
objet global JSContext, qui donne accès à une machine virtuelle JavaScript pour évaluer du code. L'environnement d'exécution
Objective-C peut aussi obtenir des références fortes vers des valeurs JavaScript via des objets JSValue.
Le protocole JSExport permet aux applications d'exposer des classes et des instances Objective-C entières à JavaScript et de les manipuler comme s'il s'agissait d'objets JavaScript.
Définir des variables et des méthodes dans un protocole qui hérite de JSExport signale à JavaScriptCore1 que ces éléments sont accessibles depuis 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
}
Dans l'exemple ci-dessus, la déclaration du protocole JSExport permet à Javascript d'accéder aux variables model et year ainsi qu'à la fonction createWith.
Maintenant que JavaScriptCore connaît le protocole CarJSExports, il peut créer un objet enveloppe approprié lorsque vous en ajoutez une instance à 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);
}
Avec cette implémentation, il devient facile d'exposer des objets à JavaScriptCore ; c'est pourquoi les développeurs doivent
veiller à n'exposer que les données nécessaires et à ne pas reproduire à l'identique la définition JSExport et le modèle de données initial.
Par exemple, si la fonction fullDetail peut contenir des données sensibles, elle ne doit pas être déclarée dans le protocole CarJSExports et ne doit l'être que dans la définition de la classe Car.
Puis-je lire un fichier de plus ?
Dans la plupart des SDK et composants WebViews, le chargement de fichiers depuis le système de fichiers est autorisé par défaut. Cela présente un risque
lorsqu'une application malveillante est capable d'ouvrir des fichiers locaux dans la WebView d'une autre application. La WebView exposée est alors
ouverte à une multitude de techniques d'exploitation, de l'abus des paramètres d'accessibilité au contournement de la politique de même origine.
Sur Android, vous pouvez désactiver l'accès au système de fichiers depuis une WebView comme suit :
webview.getSettings().setAllowFileAccess(false);
Cela n'empêchera pas la WebView de pouvoir charger des fichiers du dossier de ressources ou d'assets de l'application avec
file:///android_res et file:///android_asset. Pour verrouiller la WebView, vous ne devez pas autoriser les fichiers
chargés depuis le système de fichiers à accéder à d'autres fichiers. Cela empêchera la page chargée d'exfiltrer
des fichiers privés.
webview.getSettings().setAllowFileAccessFromFileURLs(false);
webview.getSettings().setAllowUniversalAccessFromFileURLs(false);
De plus, vous pouvez empêcher une WebView d'accéder aux content providers de l'appareil grâce au paramètre suivant :
webview.getSettings().setAllowContentAccess(false);
Voici un exemple vulnérable où un attaquant utilise un lien malveillant pour lire des données sensibles dans le répertoire de l'application. L'application écrit un mot de passe en clair dans les préférences partagées :
SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("password", "MyBadPassword");
editor.apply();
Et l'application utilise une Webview pour afficher une URL provenant d'un intent ou d'une saisie utilisateur.
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);
Dans ce cas, JavaScript et AllowFileAccessFromFileURLs sont tous deux activés. Le fichier malveillant test.html lit le
fichier de préférences partagées MainActivity.xml et peut l'exfiltrer.
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
};
En accédant à l'URL dans l'application, nous voyons que le code malveillant est exécuté et que le contenu du fichier est accessible :

Résumé
Webview est un composant important des applications mobiles. Il offre beaucoup de souplesse pour accéder à des ressources
externes et interagir avec elles, mais il s'accompagne de nombreux défis de sécurité.
En résumé, veillez à utiliser la dernière version du SDK et à éviter les composants obsolètes, et soyez prudent lorsque vous activez le code JavaScript ou implémentez des ponts entre le natif et JavaScript. Les fonctionnalités doivent être restreintes au strict nécessaire.
J'espère que vous avez trouvé cet article utile, et n'hésitez pas à tester votre application sur Ostorlab.