Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion best-practices/MASTG-BEST-0012.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
57 changes: 57 additions & 0 deletions best-practices/MASTG-BEST-0022.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
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.

Check failure on line 14 in best-practices/MASTG-BEST-0022.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Custom rule

best-practices/MASTG-BEST-0022.md:14:159 search-replace Custom rule [curly-single-quotes: Don't use curly single quotes] [Context: "column: 159 text:'’'"] https://github.com/OnkarRuikar/markdownlint-rule-search-replace

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:

Check failure on line 16 in best-practices/MASTG-BEST-0022.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Custom rule

best-practices/MASTG-BEST-0022.md:16:419 search-replace Custom rule [curly-single-quotes: Don't use curly single quotes] [Context: "column: 419 text:'’'"] https://github.com/OnkarRuikar/markdownlint-rule-search-replace

Check failure on line 16 in best-practices/MASTG-BEST-0022.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Custom rule

best-practices/MASTG-BEST-0022.md:16:144 search-replace Custom rule [curly-single-quotes: Don't use curly single quotes] [Context: "column: 144 text:'’'"] https://github.com/OnkarRuikar/markdownlint-rule-search-replace
• Automatic security updates

Check failure on line 17 in best-practices/MASTG-BEST-0022.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Hard tabs

best-practices/MASTG-BEST-0022.md:17:2 MD010/no-hard-tabs Hard tabs [Column: 2] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md
• Strong process sandboxing

Check failure on line 18 in best-practices/MASTG-BEST-0022.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Hard tabs

best-practices/MASTG-BEST-0022.md:18:2 MD010/no-hard-tabs Hard tabs [Column: 2] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md
• Built-in mitigations against common web vulnerabilities (e.g., Cross-Site Scripting (XSS), Man-in-the-Middle (MITM) attacks)

Check failure on line 19 in best-practices/MASTG-BEST-0022.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Hard tabs

best-practices/MASTG-BEST-0022.md:19:2 MD010/no-hard-tabs Hard tabs [Column: 2] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md

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)

Check failure on line 57 in best-practices/MASTG-BEST-0022.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Files should end with a single newline character

best-practices/MASTG-BEST-0022.md:57:112 MD047/single-trailing-newline Files should end with a single newline character https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md
31 changes: 31 additions & 0 deletions demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MASTestApp"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivityWebView"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.MASTestApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
android:compileSdkVersion="35"
android:compileSdkVersionCodename="15"
package="org.owasp.mastestapp"
platformBuildVersionCode="35"
platformBuildVersionName="15">
<uses-sdk
android:minSdkVersion="29"
android:targetSdkVersion="35"/>
<uses-permission android:name="android.permission.INTERNET"/>
<permission
android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature"/>
<uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
<application
android:theme="@style/Theme.MASTestApp"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:debuggable="true"
android:testOnly="true"
android:allowBackup="true"
android:supportsRtl="true"
android:extractNativeLibs="false"
android:fullBackupContent="@xml/backup_rules"
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:dataExtractionRules="@xml/data_extraction_rules">
<activity
android:theme="@style/Theme.MASTestApp"
android:name="org.owasp.mastestapp.MainActivityWebView"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name="androidx.compose.ui.tooling.PreviewActivity"
android:exported="true"/>
<activity
android:name="androidx.activity.ComponentActivity"
android:exported="true"/>
<provider
android:name="androidx.startup.InitializationProvider"
android:exported="false"
android:authorities="org.owasp.mastestapp.androidx-startup">
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
android:value="androidx.startup"/>
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup"/>
<meta-data
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
android:value="androidx.startup"/>
</provider>
<receiver
android:name="androidx.profileinstaller.ProfileInstallReceiver"
android:permission="android.permission.DUMP"
android:enabled="true"
android:exported="true"
android:directBootAware="false">
<intent-filter>
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
</intent-filter>
</receiver>
</application>
</manifest>
35 changes: 35 additions & 0 deletions demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
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 sample manifest file.

{{ ../../../../rules/mastg-android-manifest-cleartext.yml }}

And another one against the reversed Java code.

{{ ../../../../rules/mastg-android-webview-bridges.yml }}

{{ run.sh }}

### Observation

The rule detected a location where `android:usesCleartextTraffic` is set to `true` in the `AndroidManifest.xml`. It also detected three methods annotated with `@JavascriptInterface`, a location where `setJavaScriptEnabled` is set to `true`, and the location where the `WebView` component attaches a native interface through the `addJavascriptInterface()` method.

Check failure on line 29 in demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Custom rule

demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md:29:158 search-replace Custom rule [double-spaces: Avoid double spaces] [Context: "column: 158 text:'h `'"] https://github.com/OnkarRuikar/markdownlint-rule-search-replace

{{ 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.

Check failure on line 35 in demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Files should end with a single newline character

demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MASTG-DEMO-00de.md:35:365 MD047/single-trailing-newline Files should end with a single newline character https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md
85 changes: 85 additions & 0 deletions demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/MastgTestWebView.kt
Original file line number Diff line number Diff line change
@@ -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 = """
<html>
<body>
<h1>Insecure WebView Demo</h1>
<button onclick="showName()">Get Name</button>
<button onclick="showJwt()">Get JWT</button>
<button onclick="changeConfig()">Change Config</button>
<p id="output"></p>

<script>
function showName() {
var name = MASBridge.getName();
document.getElementById("output").innerText = "Name: " + name;
}

function showJwt() {
var jwt = MASBridge.getJwt();
document.getElementById("output").innerText = "JWT: " + jwt;
}

function changeConfig() {
MASBridge.changeConfiguration("newConfigValue");
document.getElementById("output").innerText = "Configuration Changed";
}
</script>
</body>
</html>

""".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
)
}
}

}
Original file line number Diff line number Diff line change
@@ -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;", "<init>", "(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;", "", "<init>", "(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/", "<html>\n<body>\n <h1>Insecure WebView Demo</h1>\n <button onclick=\"showName()\">Get Name</button>\n <button onclick=\"showJwt()\">Get JWT</button>\n <button onclick=\"changeConfig()\">Change Config</button>\n <p id=\"output\"></p>\n \n <script>\n function showName() {\n var name = MASBridge.getName();\n document.getElementById(\"output\").innerText = \"Name: \" + name;\n }\n \n function showJwt() {\n var jwt = MASBridge.getJwt();\n document.getElementById(\"output\").innerText = \"JWT: \" + jwt;\n }\n \n function changeConfig() {\n MASBridge.changeConfiguration(\"newConfigValue\");\n document.getElementById(\"output\").innerText = \"Configuration Changed\";\n }\n </script>\n</body>\n</html>\n", "text/html", "utf-8", null);
}
}
14 changes: 14 additions & 0 deletions demos/android/MASVS-PRIVACY/MASTG-DEMO-00de/output.txt
Original file line number Diff line number Diff line change
@@ -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"

Loading
Loading