diff --git a/src/com/android/settings/accounts/AccountTypePreferenceLoader.java b/src/com/android/settings/accounts/AccountTypePreferenceLoader.java index 72366d48603..3b254e9b844 100644 --- a/src/com/android/settings/accounts/AccountTypePreferenceLoader.java +++ b/src/com/android/settings/accounts/AccountTypePreferenceLoader.java @@ -33,6 +33,10 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.collection.ArraySet; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceClickListener; import androidx.preference.PreferenceFragmentCompat; @@ -46,6 +50,8 @@ import com.android.settings.utils.LocalClassLoaderContextThemeWrapper; import com.android.settingslib.accounts.AuthenticatorHelper; import com.android.settingslib.core.instrumentation.Instrumentable; +import java.util.Set; + /** * Class to load the preference screen to be added to the settings page for the specific account * type as specified in the account-authenticator. @@ -83,6 +89,7 @@ public class AccountTypePreferenceLoader { try { desc = mAuthenticatorHelper.getAccountTypeDescription(accountType); if (desc != null && desc.accountPreferencesId != 0) { + Set fragmentAllowList = generateFragmentAllowlist(parent); // Load the context of the target package, then apply the // base Settings theme (no references to local resources) // and create a context theme wrapper so that we get the @@ -99,6 +106,12 @@ public class AccountTypePreferenceLoader { themedCtx.getTheme().setTo(baseTheme); prefs = mFragment.getPreferenceManager().inflateFromResource(themedCtx, desc.accountPreferencesId, parent); + // Ignore Fragments provided dynamically, as these are coming from external + // applications which must not have access to internal Settings' fragments. + // These preferences are rendered into Settings, so they also won't have access + // to their own Fragments, meaning there is no acceptable usage of + // android:fragment here. + filterBlockedFragments(prefs, fragmentAllowList); } } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); @@ -186,6 +199,48 @@ public class AccountTypePreferenceLoader { } } + // Build allowlist from existing Fragments in PreferenceGroup + @VisibleForTesting + Set generateFragmentAllowlist(@Nullable PreferenceGroup prefs) { + Set fragmentAllowList = new ArraySet<>(); + if (prefs == null) { + return fragmentAllowList; + } + + for (int i = 0; i < prefs.getPreferenceCount(); i++) { + Preference pref = prefs.getPreference(i); + if (pref instanceof PreferenceGroup) { + fragmentAllowList.addAll(generateFragmentAllowlist((PreferenceGroup) pref)); + } + + String fragmentName = pref.getFragment(); + if (!TextUtils.isEmpty(fragmentName)) { + fragmentAllowList.add(fragmentName); + } + } + return fragmentAllowList; + } + + // Block clicks on any Preference with android:fragment that is not contained in the allowlist + @VisibleForTesting + void filterBlockedFragments(@Nullable PreferenceGroup prefs, + @NonNull Set allowedFragments) { + if (prefs == null) { + return; + } + for (int i = 0; i < prefs.getPreferenceCount(); i++) { + Preference pref = prefs.getPreference(i); + if (pref instanceof PreferenceGroup) { + filterBlockedFragments((PreferenceGroup) pref, allowedFragments); + } + + String fragmentName = pref.getFragment(); + if (fragmentName != null && !allowedFragments.contains(fragmentName)) { + pref.setOnPreferenceClickListener(preference -> true); + } + } + } + /** * Determines if the supplied Intent is safe. A safe intent is one that is * will launch a exported=true activity or owned by the same uid as the diff --git a/tests/robotests/src/com/android/settings/accounts/AccountTypePreferenceLoaderTest.java b/tests/robotests/src/com/android/settings/accounts/AccountTypePreferenceLoaderTest.java index f5c588f8d83..efa5fea7416 100644 --- a/tests/robotests/src/com/android/settings/accounts/AccountTypePreferenceLoaderTest.java +++ b/tests/robotests/src/com/android/settings/accounts/AccountTypePreferenceLoaderTest.java @@ -16,9 +16,13 @@ package com.android.settings.accounts; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -30,6 +34,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; +import androidx.collection.ArraySet; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceGroup; @@ -51,9 +56,13 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; +import java.util.Set; + @RunWith(RobolectricTestRunner.class) @Config(shadows = { com.android.settings.testutils.shadow.ShadowFragment.class, + ShadowAccountManager.class, + ShadowContentResolver.class, }) public class AccountTypePreferenceLoaderTest { @@ -63,6 +72,8 @@ public class AccountTypePreferenceLoaderTest { private PreferenceFragmentCompat mPreferenceFragment; @Mock private PackageManager mPackageManager; + @Mock + private PreferenceManager mManager; private Context mContext; private Account mAccount; @@ -91,18 +102,16 @@ public class AccountTypePreferenceLoaderTest { } @Test - @Config(shadows = {ShadowAccountManager.class, ShadowContentResolver.class}) public void updatePreferenceIntents_shouldRunRecursively() { - final PreferenceManager preferenceManager = mock(PreferenceManager.class); // Top level PreferenceGroup prefRoot = spy(new PreferenceScreen(mContext, null)); - when(prefRoot.getPreferenceManager()).thenReturn(preferenceManager); + when(prefRoot.getPreferenceManager()).thenReturn(mManager); Preference pref1 = mock(Preference.class); PreferenceGroup prefGroup2 = spy(new PreferenceScreen(mContext, null)); - when(prefGroup2.getPreferenceManager()).thenReturn(preferenceManager); + when(prefGroup2.getPreferenceManager()).thenReturn(mManager); Preference pref3 = mock(Preference.class); PreferenceGroup prefGroup4 = spy(new PreferenceScreen(mContext, null)); - when(prefGroup4.getPreferenceManager()).thenReturn(preferenceManager); + when(prefGroup4.getPreferenceManager()).thenReturn(mManager); prefRoot.addPreference(pref1); prefRoot.addPreference(prefGroup2); prefRoot.addPreference(pref3); @@ -114,7 +123,7 @@ public class AccountTypePreferenceLoaderTest { prefGroup2.addPreference(pref21); prefGroup2.addPreference(pref22); PreferenceGroup prefGroup41 = spy(new PreferenceScreen(mContext, null)); - when(prefGroup41.getPreferenceManager()).thenReturn(preferenceManager); + when(prefGroup41.getPreferenceManager()).thenReturn(mManager); Preference pref42 = mock(Preference.class); prefGroup4.addPreference(prefGroup41); prefGroup4.addPreference(pref42); @@ -132,4 +141,113 @@ public class AccountTypePreferenceLoaderTest { verify(mPrefLoader).updatePreferenceIntents(prefGroup4, acctType, mAccount); verify(mPrefLoader).updatePreferenceIntents(prefGroup41, acctType, mAccount); } + + @Test + public void generateFragmentAllowlist_nullPrefGroup_emptyList() { + Set allowed = mPrefLoader.generateFragmentAllowlist(null); + + assertThat(allowed).isEmpty(); + } + + @Test + public void generateFragmentAllowlist_simpleGroupNoFragment_emptyList() { + Preference pref = new Preference(mContext); + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + when(screen.getPreferenceManager()).thenReturn(mManager); + screen.addPreference(pref); + + Set allowed = mPrefLoader.generateFragmentAllowlist(screen); + + assertThat(allowed).isEmpty(); + } + + @Test + public void generateFragmentAllowlist_simpleGroupOneFragment_populatedList() { + Preference pref = new Preference(mContext); + pref.setFragment("test"); + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + when(screen.getPreferenceManager()).thenReturn(mManager); + screen.addPreference(pref); + + Set allowed = mPrefLoader.generateFragmentAllowlist(screen); + + assertThat(allowed).isNotEmpty(); + } + + @Test + public void generateFragmentAllowlist_nestedGroupWithFragments_populatedList() { + Preference pref = new Preference(mContext); + pref.setFragment("test"); + PreferenceScreen nested = spy(new PreferenceScreen(mContext, null)); + PreferenceScreen parent = spy(new PreferenceScreen(mContext, null)); + when(nested.getPreferenceManager()).thenReturn(mManager); + when(parent.getPreferenceManager()).thenReturn(mManager); + parent.addPreference(nested); + nested.addPreference(pref); + + Set allowed = mPrefLoader.generateFragmentAllowlist(parent); + + assertThat(allowed).isNotEmpty(); + } + + @Test + public void filterBlockedFragments_nullPrefGroup_noop() { + // verify no NPE + mPrefLoader.filterBlockedFragments(null, new ArraySet<>()); + } + + @Test + public void filterBlockedFragments_simplePrefGroupNoFragment_noop() { + Preference pref = spy(new Preference(mContext)); + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + when(screen.getPreferenceManager()).thenReturn(mManager); + screen.addPreference(pref); + + mPrefLoader.filterBlockedFragments(screen, new ArraySet<>()); + + verify(screen, never()).setOnPreferenceClickListener(any()); + verify(pref, never()).setOnPreferenceClickListener(any()); + } + + @Test + public void filterBlockedFragments_simplePrefGroupWithAllowedFragment_noop() { + Preference pref = spy(new Preference(mContext)); + pref.setFragment("test"); + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + when(screen.getPreferenceManager()).thenReturn(mManager); + screen.addPreference(pref); + + mPrefLoader.filterBlockedFragments(screen, Set.of("test")); + + verify(screen, never()).setOnPreferenceClickListener(any()); + verify(pref, never()).setOnPreferenceClickListener(any()); + } + + @Test + public void filterBlockedFragments_simplePrefGroupNoMatchFragment_overrideClick() { + Preference pref = spy(new Preference(mContext)); + pref.setFragment("test"); + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + when(screen.getPreferenceManager()).thenReturn(mManager); + screen.addPreference(pref); + + mPrefLoader.filterBlockedFragments(screen, new ArraySet<>()); + + verify(pref).setOnPreferenceClickListener(any()); + } + + @Test + public void filterBlockedFragments_nestedPrefGroupWithNoMatchFragment_overrideClick() { + Preference pref = spy(new Preference(mContext)); + pref.setFragment("test"); + PreferenceScreen nested = spy(new PreferenceScreen(mContext, null)); + PreferenceScreen parent = spy(new PreferenceScreen(mContext, null)); + when(nested.getPreferenceManager()).thenReturn(mManager); + when(parent.getPreferenceManager()).thenReturn(mManager); + parent.addPreference(nested); + nested.addPreference(pref); + + mPrefLoader.filterBlockedFragments(parent, Set.of("nomatch", "other")); + verify(pref).setOnPreferenceClickListener(any()); + } }