diff --git a/best-practices/MASTG-BEST-0012.md b/best-practices/MASTG-BEST-0012.md index deac01f5756..319a0526671 100644 --- a/best-practices/MASTG-BEST-0012.md +++ b/best-practices/MASTG-BEST-0012.md @@ -15,4 +15,4 @@ Enabling JavaScript in WebViews **increases the attack surface** and can expose Sometimes this is not possible due to app requirements. In those cases, ensure that you have implemented proper input validation, output encoding, and other security measures. -Note: sometimes you may want to use alternatives to regular WebViews, such as [Trusted Web Activities](https://developer.android.com/guide/topics/app-bundle/trusted-web-activities) or [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/), which provide a more secure way to display web content in your app. In those cases, JavaScript is handled within the browser environment, which benefits from the latest security updates, sandboxing, and mitigations against common web vulnerabilities such as Cross-Site Scripting (XSS) and Machine-in-the-Middle (MITM) attacks. +> Note: sometimes you may want to use alternatives to regular WebViews, such as [Trusted Web Activities](https://developer.android.com/develop/ui/views/layout/webapps/trusted-web-activities) or [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/), which provide a more secure way to display web content in your app. In those cases, JavaScript is handled within the browser environment, which benefits from the latest security updates, sandboxing, and mitigations against common web vulnerabilities such as Cross-Site Scripting (XSS) and Machine-in-the-Middle (MITM) attacks. diff --git a/best-practices/MASTG-BEST-0022.md b/best-practices/MASTG-BEST-0022.md new file mode 100644 index 00000000000..eb0bb30b9b2 --- /dev/null +++ b/best-practices/MASTG-BEST-0022.md @@ -0,0 +1,58 @@ +--- +title: Ensure WebViews within organizational control +alias: ensure-webviews-within-organizational-control +id: MASTG-BEST-0022 +platform: android +--- + +### Recommendation + +WebViews in Android allow applications to render web content, but they can introduce significant security risks if not properly managed. + +Whenever possible, follow the guidance in @MASTG-BEST-0012 and load only static WebViews that are packaged within the app bundle. This approach ensures that the displayed content cannot be tampered with remotely. + +If your application must display dynamic web content from the internet, ensure that all websites loaded in your WebView are secure and under your organization's control. + +When you need to display content from external or untrusted domains, you should not load it directly in a WebView. Instead, open it in the user's default browser or use safer alternatives such as [Trusted Web Activities](https://developer.android.com/guide/topics/app-bundle/trusted-web-activities) or [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/). These solutions leverage the browser's isolated environment, benefiting from: + +- Automatic security updates +- Strong process sandboxing +- Built-in mitigations against common web vulnerabilities (e.g., Cross-Site Scripting (XSS), Man-in-the-Middle (MITM) attacks) + +To enforce domain control and prevent untrusted content from loading inside your app, apply the following control: + +```kotlin +webView.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val url = request?.url.toString() + Log.d("WebView", "About to load: $url") + + // You can intercept or allow it: + val outsideControl = isOutsideControl(url) + if(outsideControl){ + // Handle the case where the URL is outside your control + // For example, open it in the default browser instead + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + } + return outsideControl // return true if you want to handle it manually + } +} + +fun isOutsideControl(url: String): Boolean { + val trustedDomains = listOf("https://my-domain.com", "https://another-trusted-domain.com") + return trustedDomains.none { url.startsWith(it) } +} +``` + +### Considerations + +A trade-off of this approach is that you may lose some control over the user experience, as the user will be taken out of your app when viewing external content. However, this is a necessary compromise to ensure the security and integrity of your application. + +### References + +- [Android WebView Security Best Practices](https://developer.android.com/reference/android/webkit/WebView#security) +- [Google Safe Browsing Service](https://developer.android.com/develop/ui/views/layout/webapps/managing-webview) diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/AndroidManifest.xml b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/AndroidManifest.xml new file mode 100644 index 00000000000..82c4b733879 --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/AndroidManifest_reversed.xml b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/AndroidManifest_reversed.xml new file mode 100644 index 00000000000..8afb80ba7db --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/AndroidManifest_reversed.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md new file mode 100644 index 00000000000..337cc370173 --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md @@ -0,0 +1,31 @@ +--- +platform: android +title: Native code Exposed Through WebViews +id: MASTG-DEMO-00de +code: [kotlin] +test: MASTG-TEST-02te +--- + +### Sample + +The following demo demonstrates a `WebView` component that exposes native functionality to JavaScript through the `addJavascriptInterface()` method that both compromises the app's integrity and confidentiality. + +{{ AndroidManifest.xml }} + +### Steps + +Let's run our @MASTG-TOOL-0110 rule against the reversed Java code. + +{{ ../../../../rules/mastg-android-webview-bridges.yml }} + +{{ run.sh }} + +### Observation + +The rule detected the location of a JavaScript/Native Bridge class (with three methods annotated with `@JavascriptInterface`). The `WebView` had `setJavaScriptEnabled` set to `true`, and the Bridge was passed through the `addJavascriptInterface()` method in that WebView. + +{{ output.txt # output2.txt }} + +### Evaluation + +After reviewing the decompiled code at the location specified in the output (file and line number), we can conclude that the test fails because JavaScript is enabled in this webview, a WebView Bridge is attached, and this Bridge allows reading of sensitive data, specifically a first and a last name (PII) and a JWT via the `@JavascriptInterface` annotated methods. diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MastgTestWebView.kt b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MastgTestWebView.kt new file mode 100644 index 00000000000..85bd1be74a3 --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MastgTestWebView.kt @@ -0,0 +1,85 @@ +package org.owasp.mastestapp + +import android.content.Context +import android.webkit.WebView +import android.webkit.WebViewClient +import android.webkit.JavascriptInterface + +class MastgTestWebView (private val context: Context){ + + // Insecure demo JS bridge for testing (do NOT use in production) + inner class Bridge { + + //Affects i.e. confidentiality + @JavascriptInterface + fun getName(): String { + return "John Doe" + } + + //Affects i.e. confidentiality and/or integrity + @JavascriptInterface + fun getJwt(): String { + return "header.payload.signature" // Dummy JWT for demo purposes + } + + //Affects i.e. integrity + @JavascriptInterface + fun changeConfiguration(config: String) { + // write to app configuration or disk + } + } + + fun mastgTest(webView: WebView) { + // Intentionally insecure WebView demo + val demoHtml = """ + + +

Insecure WebView Demo

+ + + +

+ + + + + + """.trimIndent() + + webView.apply { + // Enable JavaScript (part of the failing conditions for the test) + settings.javaScriptEnabled = true + + // Add an exposed JS interface (part of the failing conditions for the test) + addJavascriptInterface(Bridge(), "MASBridge") + + // Basic client to keep navigation inside the WebView + webViewClient = object : WebViewClient() {} + + // Load HTML under a cleartext base URL to emulate mixed conditions (requires usesCleartextTraffic=true to fetch network, but base URL is enough for origin in this inline demo) + loadDataWithBaseURL( + "http://insecure.example/", // intentional cleartext origin for the demo + demoHtml, + "text/html", + "utf-8", + null + ) + } + } + +} diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MastgTestWebView_reversed.java b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MastgTestWebView_reversed.java new file mode 100644 index 00000000000..ab9bd347b71 --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MastgTestWebView_reversed.java @@ -0,0 +1,52 @@ +package org.owasp.mastestapp; + +import android.content.Context; +import android.webkit.JavascriptInterface; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import kotlin.Metadata; +import kotlin.jvm.internal.Intrinsics; + +/* compiled from: MastgTestWebView.kt */ +@Metadata(d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001:\u0001\nB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u000e\u0010\u0006\u001a\u00020\u00072\u0006\u0010\b\u001a\u00020\tR\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u000b"}, d2 = {"Lorg/owasp/mastestapp/MastgTestWebView;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "mastgTest", "", "webView", "Landroid/webkit/WebView;", "Bridge", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +/* loaded from: classes3.dex */ +public final class MastgTestWebView { + public static final int $stable = 8; + private final Context context; + + public MastgTestWebView(Context context) { + Intrinsics.checkNotNullParameter(context, "context"); + this.context = context; + } + + /* compiled from: MastgTestWebView.kt */ + @Metadata(d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\b\u0086\u0004\u0018\u00002\u00020\u0001B\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\b\u0010\u0004\u001a\u00020\u0005H\u0007J\b\u0010\u0006\u001a\u00020\u0005H\u0007J\u0010\u0010\u0007\u001a\u00020\b2\u0006\u0010\t\u001a\u00020\u0005H\u0007¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTestWebView$Bridge;", "", "", "(Lorg/owasp/mastestapp/MastgTestWebView;)V", "getName", "", "getJwt", "changeConfiguration", "", "config", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) + public final class Bridge { + public Bridge() { + } + + @JavascriptInterface + public final String getName() { + return "John Doe"; + } + + @JavascriptInterface + public final String getJwt() { + return "header.payload.signature"; + } + + @JavascriptInterface + public final void changeConfiguration(String config) { + Intrinsics.checkNotNullParameter(config, "config"); + } + } + + public final void mastgTest(WebView webView) { + Intrinsics.checkNotNullParameter(webView, "webView"); + webView.getSettings().setJavaScriptEnabled(true); + webView.addJavascriptInterface(new Bridge(), "MASBridge"); + webView.setWebViewClient(new WebViewClient() { // from class: org.owasp.mastestapp.MastgTestWebView$mastgTest$1$1 + }); + webView.loadDataWithBaseURL("http://insecure.example/", "\n\n

Insecure WebView Demo

\n \n \n \n

\n \n \n\n\n", "text/html", "utf-8", null); + } +} diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/output.txt b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/output.txt new file mode 100644 index 00000000000..014a439cb3c --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/output.txt @@ -0,0 +1,14 @@ + + +┌────────────────┐ +│ 1 Code Finding │ +└────────────────┘ + + AndroidManifest_reversed.xml + ❯❱ rules.mastg-android-manifest-cleartext + [MASVS-PLATFORM-2] android:usesCleartextTraffic="true" is set in AndroidManifest.xml. Cleartext + traffic increases risk when combined with exposed WebView bridges and enabled JavaScript. Prefer + TLS-only communication and set this to false. + + 28┆ android:usesCleartextTraffic="true" + diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/output2.txt b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/output2.txt new file mode 100644 index 00000000000..6f90e614644 --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/output2.txt @@ -0,0 +1,39 @@ + + +┌────────────────┐ +│ 1 Code Finding │ +└────────────────┘ + + MastgTestWebView_reversed.java + ❯❱ rules.mastg-android-webview-bridges + [MASVS-PLATFORM-2] Android WebView bridge is defined and passed to addJavascriptInterface(...) + $BRIDGE + + 23┆ @Metadata(d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u00 + 02\u0010\u000e\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\b\u0086\u0004\u0018\u000 + 02\u00020\u0001B\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\b\u0010\u0004\u001a\u00020\u0005H\ + u0007J\b\u0010\u0006\u001a\u00020\u0005H\u0007J\u0010\u0010\u0007\u001a\u00020\b2\u0006\u00 + 10\t\u001a\u00020\u0005H\u0007¨\u0006\n"}, d2 = + {"Lorg/owasp/mastestapp/MastgTestWebView$Bridge;", "", "", + "(Lorg/owasp/mastestapp/MastgTestWebView;)V", "getName", "", "getJwt", + "changeConfiguration", "", "config", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) + 24┆ public final class Bridge { + 25┆ public Bridge() { + 26┆ } + 27┆ + 28┆ @JavascriptInterface + 29┆ public final String getName() { + 30┆ return "John Doe"; + 31┆ } + 32┆ + 33┆ @JavascriptInterface + 34┆ public final String getJwt() { + 35┆ return "header.payload.signature"; + 36┆ } + 37┆ + 38┆ @JavascriptInterface + 39┆ public final void changeConfiguration(String config) { + 40┆ Intrinsics.checkNotNullParameter(config, "config"); + 41┆ } + 42┆ } + diff --git a/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/run.sh b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/run.sh new file mode 100644 index 00000000000..dc246b4d4c7 --- /dev/null +++ b/demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/run.sh @@ -0,0 +1,3 @@ +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-manifest-cleartext.yml ./AndroidManifest_reversed.xml > output.txt + +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-webview-bridges.yml ./MastgTestWebView_reversed.java --max-lines-per-finding 20 > output2.txt \ No newline at end of file diff --git a/rules/mastg-android-webview-bridges.yml b/rules/mastg-android-webview-bridges.yml new file mode 100644 index 00000000000..a44177e6b0e --- /dev/null +++ b/rules/mastg-android-webview-bridges.yml @@ -0,0 +1,26 @@ +rules: + - id: mastg-android-webview-bridges + severity: WARNING + message: '[MASVS-PLATFORM-2] Android WebView bridge is defined and passed to addJavascriptInterface(...)' + mode: join + join: + rules: + # 1) Capture any setJavaScriptEnabled(true) call + - id: js-enabled + languages: [java] + pattern: $WEBVIEW.getSettings().setJavaScriptEnabled(true) + + # 2) Capture any addJavascriptInterface(new $BRIDGE(...), ...) + - id: js-interface + languages: [java] + pattern: $WEBVIEW.addJavascriptInterface(new $BRIDGE(...), $_) + # 3) Capture any class definition: class $BRIDGE { ... } + - id: class-def + languages: [java] + pattern: class $BRIDGE { ... } + + # 3) Only report when the class name matches in both findings + on: + - 'js-enabled.$WEBVIEW == js-interface.$WEBVIEW' + - 'js-interface.$BRIDGE == class-def.$BRIDGE' + diff --git a/tests-beta/android/MASVS-PLATFORM/MASTG-TEST-02te.md b/tests-beta/android/MASVS-PLATFORM/MASTG-TEST-02te.md new file mode 100644 index 00000000000..581d6081784 --- /dev/null +++ b/tests-beta/android/MASVS-PLATFORM/MASTG-TEST-02te.md @@ -0,0 +1,66 @@ +--- +platform: android +title: Native code Exposed Through WebViews +id: MASTG-TEST-02te +type: [static] +weakness: MASWE-0069 +best-practices: [MASTG-BEST-0011, MASTG-BEST-0012, MASTG-BEST-0013, MASTG-BEST-0022] +profiles: [L1, L2] +--- + +## Overview + +Android apps that have WebViews may also have WebView – Native bridges. These bridges can be added via the `addJavascriptInterface` method in the `WebView` class. They enable two-way communication: native code can pass data to the WebView, and JavaScript in the WebView can call into native code. Any website loaded inside the WebView, including those outside the organization's control, can access these bridges (if configured) whenever JavaScript is enabled with `setJavaScriptEnabled(true)`. + +The weakness could become a vulnerability if the WebView allows unencrypted (non-TLS) traffic (i.e., HTTPS) in combination with an XSS attack. Please refer to @MASTG-TEST-0235 to evaluate cleartext traffic. + +> Note: +> Applications targeting API level 16 or earlier are particularly at risk of attack because this method can be used to allow JavaScript to control the host application. + +**Example Attack Scenario:** + +1. An attacker exploits an XSS vulnerability in a website loaded in the WebView. +2. The attacker uses the XSS vulnerability to execute JavaScript code that calls methods exposed by the `addJavascriptInterface` method. +3. The attacker can then read (sensitive) data or perform actions on behalf of the user, depending on the methods exposed by the interface. + +## Steps + +1. Use a tool like @MASTG-TOOL-0110 to search for references to: + + - the `setJavaScriptEnabled` method + - the `addJavascriptInterface` method + - the `@JavascriptInterface` annotation + +## Observation + +The output should contain a list of WebView instances, including the following methods and their arguments: + + - `setJavaScriptEnabled` + - `addJavascriptInterface` and their associated classes + - `@JavascriptInterface` and their associated methods + +## Evaluation + +**Fail:** + +The test fails automatically if all the following are true: + +- the application is targeting API level 16 or lower. +- `addJavascriptInterface` is used at least once. + +The test also fails automatically if all the following are true: + +- `setJavaScriptEnabled` is `true`. +- `addJavascriptInterface` is used at least once. + +The test also fails, after evaluating the `addJavascriptInterface` method(s) and `@JavascriptInterface` annotation(s), if all the following are true: + +- Sensitive data can be read through the interface methods. +- Actions that can affect the confidentiality, integrity, or availability of the application can be performed via the interface methods. + +**Pass:** + +The test passes if any of the following are true: + +- `setJavaScriptEnabled` is `false` or not used at all. +- `addJavascriptInterface` is not used at all. diff --git a/tests/android/MASVS-PLATFORM/MASTG-TEST-0033.md b/tests/android/MASVS-PLATFORM/MASTG-TEST-0033.md index 643deb695a0..31ea34032c6 100644 --- a/tests/android/MASVS-PLATFORM/MASTG-TEST-0033.md +++ b/tests/android/MASVS-PLATFORM/MASTG-TEST-0033.md @@ -9,6 +9,9 @@ masvs_v1_levels: - L1 - L2 profiles: [L1, L2] +status: deprecated +covered_by: [MASTG-TEST-xxxx] +deprecation_note: New version available in MASTG V2 --- ## Overview