diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MASTG-DEMO-0x36.md b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MASTG-DEMO-0x36.md new file mode 100644 index 00000000000..3b292c2a095 --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MASTG-DEMO-0x36.md @@ -0,0 +1,31 @@ +--- +platform: android +title: Enforced Immediate Updates with Play Core API detected using semgrep +id: MASTG-DEMO-0x36 +code: [kotlin] +test: MASTG-TEST-0x36 +--- + +### Sample + +The following code implements immediate in-app updates using the Google Play Core API by calling `startUpdateFlowForResult` with `AppUpdateOptions.newBuilder(1)`. + +{{ MastgTest.kt # MastgTest_reversed.java }} + +### Steps + +Let's run @MASTG-TOOL-0110 rules against the sample code. + +{{ ../../../../rules/mastg-android-enforced-updating.yml }} + +{{ run.sh }} + +### Observation + +The output file shows usages of the Google Play Core API enforcing immediate update. + +{{ output.txt }} + +### Evaluation + +This code correctly implements mandatory immediate updates using the Play Core API. The app calls `startUpdateFlowForResult()` with priority level 1 (IMMEDIATE), which forces the user to install the update before continuing. There is no fallback or way to bypass the update, ensuring users cannot access the app with an outdated version. diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MainActivity.kt b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MainActivity.kt new file mode 100644 index 00000000000..ae87b064799 --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MainActivity.kt @@ -0,0 +1,83 @@ +package org.owasp.mastestapp + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +const val MASTG_TEXT_TAG = "mastgTestText" + +class MainActivity : ComponentActivity() { + + private val mastgTest by lazy { MastgTest(applicationContext) } + private lateinit var appUpdateResultLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + appUpdateResultLauncher = registerForActivityResult( + ActivityResultContracts.StartIntentSenderForResult() + ) { result -> + if (result.resultCode != RESULT_OK) { + Log.e( + "MainActivity", + "Update flow was cancelled or failed! Result code: ${result.resultCode}. Re-initiating." + ) + + mastgTest.checkForUpdate(appUpdateResultLauncher) + } else { + Log.d("MainActivity", "Update accepted. The update is now in progress.") + } + } + + setContent { + MainScreen( + displayString = "App is running. Checking for mandatory updates...", + onStartClick = { + mastgTest.checkForUpdate(appUpdateResultLauncher) + } + ) + } + + mastgTest.checkForUpdate(appUpdateResultLauncher) + } + + override fun onResume() { + super.onResume() + mastgTest.resumeUpdateIfInProgress(appUpdateResultLauncher) + } +} + +@Preview +@Composable +fun MainScreen( + displayString: String = "App is running.", + onStartClick: () -> Unit = {} +) { + BaseScreen(onStartClick = onStartClick) { + Text( + modifier = Modifier + .padding(16.dp) + .testTag(MASTG_TEXT_TAG), + text = displayString, + color = Color.White, + fontSize = 16.sp, + fontFamily = FontFamily.Monospace + ) + } +} \ No newline at end of file diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MainActivityKt_reversed.java b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MainActivityKt_reversed.java new file mode 100644 index 00000000000..2942c2ca312 --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MainActivityKt_reversed.java @@ -0,0 +1,97 @@ +package org.owasp.mastestapp; + +import androidx.compose.foundation.layout.PaddingKt; +import androidx.compose.material3.TextKt; +import androidx.compose.runtime.Composer; +import androidx.compose.runtime.ComposerKt; +import androidx.compose.runtime.RecomposeScopeImplKt; +import androidx.compose.runtime.ScopeUpdateScope; +import androidx.compose.runtime.internal.ComposableLambdaKt; +import androidx.compose.ui.Modifier; +import androidx.compose.ui.graphics.Color; +import androidx.compose.ui.platform.TestTagKt; +import androidx.compose.ui.text.TextLayoutResult; +import androidx.compose.ui.text.TextStyle; +import androidx.compose.ui.text.font.FontFamily; +import androidx.compose.ui.text.font.FontStyle; +import androidx.compose.ui.text.font.FontWeight; +import androidx.compose.ui.text.style.TextAlign; +import androidx.compose.ui.text.style.TextDecoration; +import androidx.compose.ui.unit.Dp; +import androidx.compose.ui.unit.TextUnitKt; +import kotlin.Metadata; +import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; + +/* compiled from: MainActivity.kt */ +@Metadata(d1 = {"\u0000\u0018\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\u001a'\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u00012\u000e\b\u0002\u0010\u0005\u001a\b\u0012\u0004\u0012\u00020\u00030\u0006H\u0007¢\u0006\u0002\u0010\u0007\"\u000e\u0010\u0000\u001a\u00020\u0001X\u0086T¢\u0006\u0002\n\u0000¨\u0006\b"}, d2 = {"MASTG_TEXT_TAG", "", "MainScreen", "", "displayString", "onStartClick", "Lkotlin/Function0;", "(Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V", "app_debug"}, k = 2, mv = {2, 0, 0}, xi = 48) +/* loaded from: classes3.dex */ +public final class MainActivityKt { + public static final String MASTG_TEXT_TAG = "mastgTestText"; + + /* JADX INFO: Access modifiers changed from: private */ + public static final Unit MainScreen$lambda$1(String str, Function0 function0, int i, int i2, Composer composer, int i3) { + MainScreen(str, function0, composer, RecomposeScopeImplKt.updateChangedFlags(i | 1), i2); + return Unit.INSTANCE; + } + + public static final void MainScreen(final String displayString, final Function0 function0, Composer $composer, final int $changed, final int i) { + Composer $composer2 = $composer.startRestartGroup(-958245146); + ComposerKt.sourceInformation($composer2, "C(MainScreen)101@4458L280,101@4418L320:MainActivity.kt#vyvp3i"); + int $dirty = $changed; + int i2 = i & 1; + if (i2 != 0) { + $dirty |= 6; + } else if (($changed & 14) == 0) { + $dirty |= $composer2.changed(displayString) ? 4 : 2; + } + int i3 = i & 2; + if (i3 != 0) { + $dirty |= 48; + } else if (($changed & 112) == 0) { + $dirty |= $composer2.changedInstance(function0) ? 32 : 16; + } + if (($dirty & 91) != 18 || !$composer2.getSkipping()) { + if (i2 != 0) { + displayString = "App is running."; + } + if (i3 != 0) { + function0 = new Function0() { // from class: org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda0 + @Override // kotlin.jvm.functions.Function0 + public final Object invoke() { + return Unit.INSTANCE; + } + }; + } + BaseScreenKt.BaseScreen(function0, ComposableLambdaKt.rememberComposableLambda(891526107, true, new Function2() { // from class: org.owasp.mastestapp.MainActivityKt.MainScreen.2 + @Override // kotlin.jvm.functions.Function2 + public /* bridge */ /* synthetic */ Unit invoke(Composer composer, Integer num) { + invoke(composer, num.intValue()); + return Unit.INSTANCE; + } + + public final void invoke(Composer $composer3, int $changed2) { + ComposerKt.sourceInformation($composer3, "C102@4468L264:MainActivity.kt#vyvp3i"); + if (($changed2 & 11) != 2 || !$composer3.getSkipping()) { + TextKt.m2715Text4IGK_g(displayString, TestTagKt.testTag(PaddingKt.m681padding3ABfNKs(Modifier.INSTANCE, Dp.m6664constructorimpl(16)), MainActivityKt.MASTG_TEXT_TAG), Color.INSTANCE.m4219getWhite0d7_KjU(), TextUnitKt.getSp(16), (FontStyle) null, (FontWeight) null, (FontFamily) FontFamily.INSTANCE.getMonospace(), 0L, (TextDecoration) null, (TextAlign) null, 0L, 0, false, 0, 0, (Function1) null, (TextStyle) null, $composer3, 3504, 0, 130992); + return; + } + $composer3.skipToGroupEnd(); + } + }, $composer2, 54), $composer2, (($dirty >> 3) & 14) | 48, 0); + } else { + $composer2.skipToGroupEnd(); + } + ScopeUpdateScope scopeUpdateScopeEndRestartGroup = $composer2.endRestartGroup(); + if (scopeUpdateScopeEndRestartGroup != null) { + scopeUpdateScopeEndRestartGroup.updateScope(new Function2() { // from class: org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda1 + @Override // kotlin.jvm.functions.Function2 + public final Object invoke(Object obj, Object obj2) { + return MainActivityKt.MainScreen$lambda$1(displayString, function0, $changed, i, (Composer) obj, ((Integer) obj2).intValue()); + } + }); + } + } +} diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MastgTest.kt b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MastgTest.kt new file mode 100644 index 00000000000..b96f7020212 --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MastgTest.kt @@ -0,0 +1,62 @@ +package org.owasp.mastestapp + +import android.content.Context +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability + +class MastgTest(context: Context) { + + private val appUpdateManager: AppUpdateManager = AppUpdateManagerFactory.create(context) + + /** + * Checks if an IMMEDIATE update is available on the Play Store. + */ + fun checkForUpdate( + appUpdateResultLauncher: ActivityResultLauncher + ) { + Log.d("MastgTest", "Checking for an update...") + val appUpdateInfoTask = appUpdateManager.appUpdateInfo + + appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> + val isUpdateAvailable = appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE + val isImmediateUpdateAllowed = appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) + + if (isUpdateAvailable && isImmediateUpdateAllowed) { + Log.d("MastgTest", "Immediate update available. Starting flow.") + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + appUpdateResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build() + ) + } else { + Log.d("MastgTest", "No immediate update available.") + } + }.addOnFailureListener { e -> + Log.e("MastgTest", "Failed to check for updates.", e) + } + } + + /** + * Resumes an update that is already in progress. This is critical for onResume(). + */ + fun resumeUpdateIfInProgress( + appUpdateResultLauncher: ActivityResultLauncher + ) { + appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) { + Log.d("MastgTest", "Resuming in-progress update.") + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + appUpdateResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build() + ) + } + } + } +} diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MastgTest_reversed.java b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MastgTest_reversed.java new file mode 100644 index 00000000000..47cf08633ca --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/MastgTest_reversed.java @@ -0,0 +1,117 @@ +package org.owasp.mastestapp; + +import android.content.Context; +import android.util.Log; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.IntentSenderRequest; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; +import com.google.android.play.core.appupdate.AppUpdateInfo; +import com.google.android.play.core.appupdate.AppUpdateManager; +import com.google.android.play.core.appupdate.AppUpdateManagerFactory; +import com.google.android.play.core.appupdate.AppUpdateOptions; +import kotlin.Metadata; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.internal.Intrinsics; + +/* compiled from: MastgTest.kt */ +@Metadata(d1 = {"\u0000*\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0014\u0010\b\u001a\u00020\t2\f\u0010\n\u001a\b\u0012\u0004\u0012\u00020\f0\u000bJ\u0014\u0010\r\u001a\u00020\t2\f\u0010\n\u001a\b\u0012\u0004\u0012\u00020\f0\u000bR\u000e\u0010\u0006\u001a\u00020\u0007X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u000e"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "appUpdateManager", "Lcom/google/android/play/core/appupdate/AppUpdateManager;", "checkForUpdate", "", "appUpdateResultLauncher", "Landroidx/activity/result/ActivityResultLauncher;", "Landroidx/activity/result/IntentSenderRequest;", "resumeUpdateIfInProgress", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +/* loaded from: classes3.dex */ +public final class MastgTest { + public static final int $stable = 8; + private final AppUpdateManager appUpdateManager; + + public MastgTest(Context context) { + Intrinsics.checkNotNullParameter(context, "context"); + AppUpdateManager appUpdateManagerCreate = AppUpdateManagerFactory.create(context); + Intrinsics.checkNotNullExpressionValue(appUpdateManagerCreate, "create(...)"); + this.appUpdateManager = appUpdateManagerCreate; + } + + public final void checkForUpdate(final ActivityResultLauncher appUpdateResultLauncher) { + Intrinsics.checkNotNullParameter(appUpdateResultLauncher, "appUpdateResultLauncher"); + Log.d("MastgTest", "Checking for an update..."); + Task appUpdateInfoTask = this.appUpdateManager.getAppUpdateInfo(); + Intrinsics.checkNotNullExpressionValue(appUpdateInfoTask, "getAppUpdateInfo(...)"); + final Function1 function1 = new Function1() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0 + @Override // kotlin.jvm.functions.Function1 + public final Object invoke(Object obj) { + return MastgTest.checkForUpdate$lambda$0(this.f$0, appUpdateResultLauncher, (AppUpdateInfo) obj); + } + }; + appUpdateInfoTask.addOnSuccessListener(new OnSuccessListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda1 + @Override // com.google.android.gms.tasks.OnSuccessListener + public final void onSuccess(Object obj) { + MastgTest.checkForUpdate$lambda$1(function1, obj); + } + }).addOnFailureListener(new OnFailureListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda2 + @Override // com.google.android.gms.tasks.OnFailureListener + public final void onFailure(Exception exc) { + MastgTest.checkForUpdate$lambda$2(exc); + } + }); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final void checkForUpdate$lambda$1(Function1 tmp0, Object p0) { + Intrinsics.checkNotNullParameter(tmp0, "$tmp0"); + tmp0.invoke(p0); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final Unit checkForUpdate$lambda$0(MastgTest this$0, ActivityResultLauncher appUpdateResultLauncher, AppUpdateInfo appUpdateInfo) { + Intrinsics.checkNotNullParameter(this$0, "this$0"); + Intrinsics.checkNotNullParameter(appUpdateResultLauncher, "$appUpdateResultLauncher"); + boolean isUpdateAvailable = appUpdateInfo.updateAvailability() == 2; + boolean isImmediateUpdateAllowed = appUpdateInfo.isUpdateTypeAllowed(1); + if (!isUpdateAvailable || !isImmediateUpdateAllowed) { + Log.d("MastgTest", "No immediate update available."); + } else { + Log.d("MastgTest", "Immediate update available. Starting flow."); + this$0.appUpdateManager.startUpdateFlowForResult(appUpdateInfo, appUpdateResultLauncher, AppUpdateOptions.newBuilder(1).build()); + } + return Unit.INSTANCE; + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final void checkForUpdate$lambda$2(Exception e) { + Intrinsics.checkNotNullParameter(e, "e"); + Log.e("MastgTest", "Failed to check for updates.", e); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final void resumeUpdateIfInProgress$lambda$4(Function1 tmp0, Object p0) { + Intrinsics.checkNotNullParameter(tmp0, "$tmp0"); + tmp0.invoke(p0); + } + + public final void resumeUpdateIfInProgress(final ActivityResultLauncher appUpdateResultLauncher) { + Intrinsics.checkNotNullParameter(appUpdateResultLauncher, "appUpdateResultLauncher"); + Task appUpdateInfo = this.appUpdateManager.getAppUpdateInfo(); + final Function1 function1 = new Function1() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda3 + @Override // kotlin.jvm.functions.Function1 + public final Object invoke(Object obj) { + return MastgTest.resumeUpdateIfInProgress$lambda$3(this.f$0, appUpdateResultLauncher, (AppUpdateInfo) obj); + } + }; + appUpdateInfo.addOnSuccessListener(new OnSuccessListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda4 + @Override // com.google.android.gms.tasks.OnSuccessListener + public final void onSuccess(Object obj) { + MastgTest.resumeUpdateIfInProgress$lambda$4(function1, obj); + } + }); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final Unit resumeUpdateIfInProgress$lambda$3(MastgTest this$0, ActivityResultLauncher appUpdateResultLauncher, AppUpdateInfo appUpdateInfo) { + Intrinsics.checkNotNullParameter(this$0, "this$0"); + Intrinsics.checkNotNullParameter(appUpdateResultLauncher, "$appUpdateResultLauncher"); + if (appUpdateInfo.updateAvailability() == 3) { + Log.d("MastgTest", "Resuming in-progress update."); + this$0.appUpdateManager.startUpdateFlowForResult(appUpdateInfo, appUpdateResultLauncher, AppUpdateOptions.newBuilder(1).build()); + } + return Unit.INSTANCE; + } +} diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/build.gradle.kts.libs b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/build.gradle.kts.libs new file mode 100644 index 00000000000..383c18b99ae --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/build.gradle.kts.libs @@ -0,0 +1 @@ +implementation("com.google.android.play:app-update-ktx:2.1.0") \ No newline at end of file diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/output.txt b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/output.txt new file mode 100644 index 00000000000..ab33fb75e54 --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/output.txt @@ -0,0 +1,15 @@ + + +┌─────────────────┐ +│ 2 Code Findings │ +└─────────────────┘ + + MastgTest_reversed.java + ❯❱rules.mastg-android-enforced-updating + [MASVS-CODE-1] immediate in-app update enforcement detected. + + 73┆ this$0.appUpdateManager.startUpdateFlowForResult(appUpdateInfo, appUpdateResultLauncher, + AppUpdateOptions.newBuilder(1).build()); + ⋮┆---------------------------------------- + 113┆ this$0.appUpdateManager.startUpdateFlowForResult(appUpdateInfo, appUpdateResultLauncher, + AppUpdateOptions.newBuilder(1).build()); diff --git a/demos/android/MASVS-CODE/MASTG-DEMO-0x36/run.sh b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/run.sh new file mode 100755 index 00000000000..5c2cbfe9d35 --- /dev/null +++ b/demos/android/MASVS-CODE/MASTG-DEMO-0x36/run.sh @@ -0,0 +1 @@ +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-enforced-updating.yml ./MastgTest_reversed.java > output.txt \ No newline at end of file diff --git a/rules/mastg-android-enforced-updating.yml b/rules/mastg-android-enforced-updating.yml new file mode 100644 index 00000000000..1291d937197 --- /dev/null +++ b/rules/mastg-android-enforced-updating.yml @@ -0,0 +1,12 @@ +rules: + - id: mastg-android-enforced-updating + severity: WARNING + languages: + - java + metadata: + summary: This rule detects usage of Play Core's immediate in-app update enforcement + message: "[MASVS-CODE-1] immediate in-app update enforcement detected." + patterns: + - pattern-either: + - pattern: $OBJ.startUpdateFlowForResult($INFO, $LAUNCHER, + AppUpdateOptions.newBuilder(1).build()) \ No newline at end of file diff --git a/tests-beta/android/MASVS-CODE/MASTG-TEDT-0x36-1.md b/tests-beta/android/MASVS-CODE/MASTG-TEDT-0x36-1.md new file mode 100644 index 00000000000..4645539d7a6 --- /dev/null +++ b/tests-beta/android/MASVS-CODE/MASTG-TEDT-0x36-1.md @@ -0,0 +1,33 @@ +--- +title: Verifying Mandatory In-App Update Enforcement using MITM Proxy +platform: android +id: MASTG-TEST-0x36-1 +type: [dynamic] +weakness: MASWE-0075 +profiles: [L2] +--- + +## Overview + +The goal of the test is to verify whether the app properly enforces mandatory updates, When using a MITM proxy to send a version that the backend considers unsupported and verify if the app correctly blocks access and requires the user to update before continuing. + +## Steps + +1. Set up a MITM proxy using @MASTG-TECH-0011 to intercept network traffic. +2. Launch the app and identify API calls that transmit version information (e.g., `X-App-Version`, `version`, `build`, `minVersion` in headers, parameters, or request body). +3. Modify the intercepted request to indicate that the current app version is unsupported (e.g., change `version` to an older version or set `minVersion` to a value higher than the current version). +4. Forward the modified request to the backend. + +## Observation + +The output should contain the app's response when an unsupported version is sent to the backend: + +- The backend's response indicating the version is outdated (e.g., a response code, JSON field, or message stating the update is required). +- Whether the app displays a blocking dialog or screen that prevents further use until the update is completed. +- Whether the user can dismiss the update prompt and continue using the app, or if the app completely blocks access to functionality. + +## Evaluation + +The test case fails if: + +The app does not block access to the application functionality when the backend indicates the version is unsupported and does not trigger a mandatory update flow through either the backend mechanism or the Play In-App Updates API. diff --git a/tests-beta/android/MASVS-CODE/MASTG-TEST-0x36.md b/tests-beta/android/MASVS-CODE/MASTG-TEST-0x36.md new file mode 100644 index 00000000000..257d807c009 --- /dev/null +++ b/tests-beta/android/MASVS-CODE/MASTG-TEST-0x36.md @@ -0,0 +1,24 @@ +--- +title: Enforcing Mandatory In-App Updates +platform: android +id: MASTG-TEST-0x36 +type: [static] +weakness: MASWE-0075 +profiles: [L2] +--- + +## Overview + +The goal of this test is to verify whether the application enforces mandatory updates, preventing users from accessing the app until the latest version has been successfully downloaded and installed. A mandatory update can typically be achieved by using the [Google Play Core In-App Update API](https://developer.android.com/guide/playcore/in-app-updates/kotlin-java) and invoking `startUpdateFlowForResult` with an Immediate update type option `AppUpdateType.IMMEDIATE` or value `1`. + +## Steps + +1. Run a static analysis tool such as @MASTG-TOOL-0110 on codebase for usages of the calls to the Play Core in-app update API, specifically `startUpdateFlowForResult`, that are configured with the integer value `1` (`AppUpdateType.IMMEDIATE`). + +## Observation + +The output should contain the locations where `startUpdateFlowForResult` with `AppUpdateOptions.newBuilder(1).build()` is called. + +## Evaluation + +The test fails if the app does not implement enforced updating using Play In-App Updates API. diff --git a/tests/android/MASVS-CODE/MASTG-TEST-0036.md b/tests/android/MASVS-CODE/MASTG-TEST-0036.md index 41625857ee7..ef4e4d6f6ea 100644 --- a/tests/android/MASVS-CODE/MASTG-TEST-0036.md +++ b/tests/android/MASVS-CODE/MASTG-TEST-0036.md @@ -8,6 +8,9 @@ title: Testing Enforced Updating masvs_v1_levels: - L2 profiles: [L2] +status: deprecated +covered_by: [MASTG-TEST-0x36] +deprecation_note: New version available in MASTG V2 --- ## Overview @@ -84,7 +87,7 @@ protected void onResume() { } ``` ->Source: [https://developer.android.com/guide/app-bundle/in-app-updates](https://developer.android.com/guide/app-bundle/in-app-updates "Support in-app updates") +> Source: [https://developer.android.com/guide/app-bundle/in-app-updates](https://developer.android.com/guide/app-bundle/in-app-updates "Support in-app updates") ## Dynamic analysis