Add CreatorToken and CreatorTokenInfo
Add necessary data structure of platform to defend Intent Redirect attack
Bug: 363016360
Test: IntentTest
Flag: android.security.prevent_intent_redirect
Change-Id: I8b53a153e22fc7feecec66441d27749f71612e9e
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 031380d..044178c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -20,6 +20,7 @@
import static android.content.ContentProvider.maybeAddUserId;
import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
import static android.security.Flags.FLAG_FRP_ENFORCEMENT;
+import static android.security.Flags.preventIntentRedirect;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
@@ -7687,9 +7688,17 @@
/** @hide */
public static final int LOCAL_FLAG_FROM_SYSTEM = 1 << 5;
+ /**
+ * This flag indicates the creator token of this intent has been verified.
+ *
+ * @hide
+ */
+ public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6;
+
/** @hide */
@IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = {
EXTENDED_FLAG_FILTER_MISMATCH,
+ EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ExtendedFlags {}
@@ -7703,6 +7712,13 @@
@TestApi
public static final int EXTENDED_FLAG_FILTER_MISMATCH = 1 << 0;
+ /**
+ * This flag indicates the creator token of this intent is either missing or invalid.
+ *
+ * @hide
+ */
+ public static final int EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN = 1 << 1;
+
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// toUri() and parseUri() options.
@@ -7870,6 +7886,7 @@
this.mPackage = o.mPackage;
this.mComponent = o.mComponent;
this.mOriginalIntent = o.mOriginalIntent;
+ this.mCreatorTokenInfo = o.mCreatorTokenInfo;
if (o.mCategories != null) {
this.mCategories = new ArraySet<>(o.mCategories);
@@ -12176,6 +12193,60 @@
return (mExtras != null) ? mExtras.describeContents() : 0;
}
+ private static class CreatorTokenInfo {
+ // Stores a creator token for an intent embedded as an extra intent in a top level intent,
+ private IBinder mCreatorToken;
+ // Stores all extra keys whose values are intents for a top level intent.
+ private ArraySet<String> mExtraIntentKeys;
+ }
+
+ private @Nullable CreatorTokenInfo mCreatorTokenInfo;
+
+ /** @hide */
+ public void removeCreatorTokenInfo() {
+ mCreatorTokenInfo = null;
+ }
+
+ /** @hide */
+ public @Nullable IBinder getCreatorToken() {
+ return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mCreatorToken;
+ }
+
+ /** @hide */
+ public Set<String> getExtraIntentKeys() {
+ return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mExtraIntentKeys;
+ }
+
+ /** @hide */
+ public void setCreatorToken(@NonNull IBinder creatorToken) {
+ if (mCreatorTokenInfo == null) {
+ mCreatorTokenInfo = new CreatorTokenInfo();
+ }
+ mCreatorTokenInfo.mCreatorToken = creatorToken;
+ }
+
+ /**
+ * Collects keys in the extra bundle whose value are intents.
+ * @hide
+ */
+ public void collectExtraIntentKeys() {
+ if (!preventIntentRedirect()) return;
+
+ if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) {
+ for (String key : mExtras.keySet()) {
+ if (mExtras.get(key) instanceof Intent) {
+ if (mCreatorTokenInfo == null) {
+ mCreatorTokenInfo = new CreatorTokenInfo();
+ }
+ if (mCreatorTokenInfo.mExtraIntentKeys == null) {
+ mCreatorTokenInfo.mExtraIntentKeys = new ArraySet<>();
+ }
+ mCreatorTokenInfo.mExtraIntentKeys.add(key);
+ }
+ }
+ }
+ }
+
public void writeToParcel(Parcel out, int flags) {
out.writeString8(mAction);
Uri.writeToParcel(out, mData);
@@ -12225,6 +12296,16 @@
} else {
out.writeInt(0);
}
+
+ if (preventIntentRedirect()) {
+ if (mCreatorTokenInfo == null) {
+ out.writeInt(0);
+ } else {
+ out.writeInt(1);
+ out.writeStrongBinder(mCreatorTokenInfo.mCreatorToken);
+ out.writeArraySet(mCreatorTokenInfo.mExtraIntentKeys);
+ }
+ }
}
public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR
@@ -12282,6 +12363,14 @@
if (in.readInt() != 0) {
mOriginalIntent = new Intent(in);
}
+
+ if (preventIntentRedirect()) {
+ if (in.readInt() != 0) {
+ mCreatorTokenInfo = new CreatorTokenInfo();
+ mCreatorTokenInfo.mCreatorToken = in.readStrongBinder();
+ mCreatorTokenInfo.mExtraIntentKeys = (ArraySet<String>) in.readArraySet(null);
+ }
+ }
}
/**
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 56d3669..45e9def 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -52,3 +52,11 @@
description: "Opt the system into enforcement of BAL"
bug: "339403750"
}
+
+flag {
+ name: "prevent_intent_redirect"
+ namespace: "responsible_apis"
+ description: "Prevent intent redirect attacks"
+ bug: "361143368"
+ is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index d98836f..e9ce712 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -104,6 +104,7 @@
"mockito-target-extended-minus-junit4",
"TestParameterInjector",
"android.content.res.flags-aconfig-java",
+ "android.security.flags-aconfig-java",
],
libs: [
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
new file mode 100644
index 0000000..d169ce3
--- /dev/null
+++ b/core/tests/coretests/src/android/content/IntentTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.security.Flags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksCoreTests:IntentTest
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IntentTest {
+ private static final String TEST_ACTION = "android.content.IntentTest_test";
+ private static final String TEST_EXTRA_NAME = "testExtraName";
+ private static final Uri TEST_URI = Uri.parse("content://com.example/people");
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
+ public void testReadFromParcelWithExtraIntentKeys() {
+ Intent intent = new Intent("TEST_ACTION");
+ intent.putExtra(TEST_EXTRA_NAME, new Intent(TEST_ACTION));
+ intent.putExtra(TEST_EXTRA_NAME + "2", 1);
+
+ intent.collectExtraIntentKeys();
+ final Parcel parcel = Parcel.obtain();
+ intent.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ final Intent target = new Intent();
+ target.readFromParcel(parcel);
+
+ assertEquals(intent.getAction(), target.getAction());
+ assertEquals(intent.getExtraIntentKeys(), target.getExtraIntentKeys());
+ assertThat(intent.getExtraIntentKeys()).hasSize(1);
+ }
+
+ @Test
+ public void testCreatorTokenInfo() {
+ Intent intent = new Intent(TEST_ACTION);
+ IBinder creatorToken = new Binder();
+
+ intent.setCreatorToken(creatorToken);
+ assertThat(intent.getCreatorToken()).isEqualTo(creatorToken);
+
+ intent.removeCreatorTokenInfo();
+ assertThat(intent.getCreatorToken()).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
+ public void testCollectExtraIntentKeys() {
+ Intent intent = new Intent(TEST_ACTION);
+ Intent extraIntent = new Intent(TEST_ACTION, TEST_URI);
+ intent.putExtra(TEST_EXTRA_NAME, extraIntent);
+
+ intent.collectExtraIntentKeys();
+
+ assertThat(intent.getExtraIntentKeys()).hasSize(1);
+ assertThat(intent.getExtraIntentKeys()).contains(TEST_EXTRA_NAME);
+ }
+
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 3c57476..7ff6832 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -262,6 +262,7 @@
import android.content.AttributionSource;
import android.content.AutofillOptions;
import android.content.BroadcastReceiver;
+import android.content.ClipData;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
@@ -418,7 +419,6 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.MemInfoReader;
import com.android.internal.util.Preconditions;
-import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.AlarmManagerInternal;
import com.android.server.BootReceiver;
import com.android.server.DeviceIdleInternal;
@@ -438,6 +438,7 @@
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
import com.android.server.contentcapture.ContentCaptureManagerInternal;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.criticalevents.CriticalEventLog;
import com.android.server.firewall.IntentFirewall;
import com.android.server.graphics.fonts.FontManagerInternal;
@@ -482,6 +483,7 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -499,6 +501,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -19078,4 +19081,87 @@
Freezer getFreezer() {
return mFreezer;
}
+
+ // Set of IntentCreatorToken objects that are currently active.
+ private static final Map<IntentCreatorToken.Key, WeakReference<IntentCreatorToken>>
+ sIntentCreatorTokenCache = new ConcurrentHashMap<>();
+
+ /**
+ * A binder token used to keep track of which app created the intent. This token can be used to
+ * defend against intent redirect attacks. It stores uid of the intent creator and key fields of
+ * the intent to make it impossible for attacker to fake uid with a malicious intent.
+ *
+ * @hide
+ */
+ public static final class IntentCreatorToken extends Binder {
+ @NonNull
+ private final Key mKeyFields;
+
+ public IntentCreatorToken(int creatorUid, Intent intent) {
+ super();
+ this.mKeyFields = new Key(creatorUid, intent);
+ }
+
+ public int getCreatorUid() {
+ return mKeyFields.mCreatorUid;
+ }
+
+ /** {@hide} */
+ public static boolean isValid(@NonNull Intent intent) {
+ IBinder binder = intent.getCreatorToken();
+ IntentCreatorToken token = null;
+ if (binder instanceof IntentCreatorToken) {
+ token = (IntentCreatorToken) binder;
+ }
+ return token != null && token.mKeyFields.equals(
+ new Key(token.mKeyFields.mCreatorUid, intent));
+ }
+
+ private static class Key {
+ private Key(int creatorUid, Intent intent) {
+ this.mCreatorUid = creatorUid;
+ this.mAction = intent.getAction();
+ this.mData = intent.getData();
+ this.mType = intent.getType();
+ this.mPackage = intent.getPackage();
+ this.mComponent = intent.getComponent();
+ this.mFlags = intent.getFlags() & Intent.IMMUTABLE_FLAGS;
+ ClipData clipData = intent.getClipData();
+ if (clipData != null) {
+ this.mClipDataUris = new ArrayList<>(clipData.getItemCount());
+ for (int i = 0; i < clipData.getItemCount(); i++) {
+ this.mClipDataUris.add(clipData.getItemAt(i).getUri());
+ }
+ }
+ }
+
+ private final int mCreatorUid;
+ private final String mAction;
+ private final Uri mData;
+ private final String mType;
+ private final String mPackage;
+ private final ComponentName mComponent;
+ private final int mFlags;
+ private List<Uri> mClipDataUris;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Key key = (Key) o;
+ return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals(
+ mAction, key.mAction) && Objects.equals(mData, key.mData)
+ && Objects.equals(mType, key.mType) && Objects.equals(mPackage,
+ key.mPackage) && Objects.equals(mComponent, key.mComponent)
+ && Objects.equals(mClipDataUris, key.mClipDataUris);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent,
+ mFlags,
+ mClipDataUris);
+ }
+ }
+ }
}