Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 027405f6 authored by Iavor-Valentin Iftime's avatar Iavor-Valentin Iftime Committed by Evelyn Torres
Browse files

[BACKPORT] Check sound Uri permission when creating a notification channel

 Verify that the calling app has the right permissions to read/grant the sound Uri
 for a channel it creates.
 Previously, Uri permissions would only be checked by NotificationRecord when a notification was posted.

Flag: EXEMPT security fix

Test: atest PreferencesHelperTest
Test: atest NotificationManagerServiceTest
Bug: 337775777

(cherry picked from commit 94fc92cf)
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:665e1aee2dbad19d436a08d753b4d5cf733abeb5)
Merged-In: If5a6c80e3bd665a9e67655a230e43d704422082b
Change-Id: If5a6c80e3bd665a9e67655a230e43d704422082b
parent e514efcb
Loading
Loading
Loading
Loading
+2 −7
Original line number Diff line number Diff line
@@ -2240,6 +2240,7 @@ public class NotificationManagerService extends SystemService {
                mZenModeHelper,
                new NotificationChannelLoggerImpl(),
                mAppOps,
                mUgmInternal,
                new SysUiStatsEvent.BuilderFactory());
        mRankingHelper = new RankingHelper(getContext(),
                mRankingHandler,
@@ -5563,13 +5564,7 @@ public class NotificationManagerService extends SystemService {
            final Uri originalSoundUri =
                    (originalChannel != null) ? originalChannel.getSound() : null;
            if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) {
                Binder.withCleanCallingIdentity(() -> {
                    mUgmInternal.checkGrantUriPermission(sourceUid, null,
                            ContentProvider.getUriWithoutUserId(soundUri),
                            Intent.FLAG_GRANT_READ_URI_PERMISSION,
                            ContentProvider.getUserIdFromUri(soundUri,
                            UserHandle.getUserId(sourceUid)));
                });
                PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid);
            }
        }

+4 −9
Original line number Diff line number Diff line
@@ -1342,19 +1342,16 @@ public final class NotificationRecord {
     * {@link SecurityException} depending on target SDK of enqueuing app.
     */
    private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
        if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
        if (mGrantableUris != null && mGrantableUris.contains(uri)) {
            return; // already verified this URI
        }

        // We can't grant Uri permissions from system
        final int sourceUid = getSbn().getUid();
        if (sourceUid == android.os.Process.SYSTEM_UID) return;

        final long ident = Binder.clearCallingIdentity();
        try {
            // This will throw SecurityException if caller can't grant
            mUgmInternal.checkGrantUriPermission(sourceUid, null,
                    ContentProvider.getUriWithoutUserId(uri),
                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
                    ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
            PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid);

            if (mGrantableUris == null) {
                mGrantableUris = new ArraySet<>();
@@ -1374,8 +1371,6 @@ public final class NotificationRecord {
                    }
                }
            }
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 com.android.server.notification;

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.UserHandle;

import com.android.server.uri.UriGrantsManagerInternal;

/**
 * NotificationManagerService helper for querying/setting the app-level notification permission
 */
public final class PermissionHelper {
    private static final String TAG = "PermissionHelper";

    static void grantUriPermission(final UriGrantsManagerInternal ugmInternal, Uri uri,
            int sourceUid) {
        if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;

        Binder.withCleanCallingIdentity(() -> {
            // This will throw a SecurityException if the caller can't grant.
            ugmInternal.checkGrantUriPermission(sourceUid, null,
                    ContentProvider.getUriWithoutUserId(uri),
                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
                    ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
        });
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -68,6 +68,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.server.uri.UriGrantsManagerInternal;

import org.json.JSONArray;
import org.json.JSONException;
@@ -176,6 +177,7 @@ public class PreferencesHelper implements RankingConfig {
    private final ZenModeHelper mZenModeHelper;
    private final NotificationChannelLogger mNotificationChannelLogger;
    private final AppOpsManager mAppOps;
    private final UriGrantsManagerInternal mUgmInternal;

    private SparseBooleanArray mBadgingEnabled;
    private SparseBooleanArray mBubblesEnabled;
@@ -194,6 +196,7 @@ public class PreferencesHelper implements RankingConfig {
    public PreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
            ZenModeHelper zenHelper, NotificationChannelLogger notificationChannelLogger,
            AppOpsManager appOpsManager,
            UriGrantsManagerInternal ugmInternal,
            SysUiStatsEvent.BuilderFactory statsEventBuilderFactory) {
        mContext = context;
        mZenModeHelper = zenHelper;
@@ -202,6 +205,7 @@ public class PreferencesHelper implements RankingConfig {
        mNotificationChannelLogger = notificationChannelLogger;
        mAppOps = appOpsManager;
        mStatsEventBuilderFactory = statsEventBuilderFactory;
        mUgmInternal = ugmInternal;

        updateBadgingEnabled();
        updateBubblesEnabled();
@@ -963,6 +967,12 @@ public class PreferencesHelper implements RankingConfig {
                        : NotificationChannel.DEFAULT_ALLOW_BUBBLE);
            }
            clearLockedFieldsLocked(channel);

            // Verify that the app has permission to read the sound Uri
            // Only check for new channels, as regular apps can only set sound
            // before creating. See: {@link NotificationChannel#setSound}
            PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);

            channel.setImportanceLockedByOEM(r.oemLockedImportance);
            if (!channel.isImportanceLockedByOEM()) {
                if (r.oemLockedChannels.contains(channel.getId())) {
+36 −1
Original line number Diff line number Diff line
@@ -2694,6 +2694,41 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
                .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri),
                anyInt(), eq(Process.myUserHandle().getIdentifier()));

        mBinderService.updateNotificationChannelFromPrivilegedListener(
                null, mPkg, Process.myUserHandle(), updatedNotificationChannel);

        verify(mPreferencesHelper, times(1)).updateNotificationChannel(
                anyString(), anyInt(), any(), anyBoolean());

        verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg),
                eq(Process.myUserHandle()), eq(mTestNotificationChannel),
                eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
    }

    @Test
    public void
        testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm()
            throws Exception {
        mService.setPreferencesHelper(mPreferencesHelper);
        when(mCompanionMgr.getAssociations(mPkg, mUserId))
                .thenReturn(singletonList(mock(AssociationInfo.class)));
        when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(),
                eq(mTestNotificationChannel.getId()), anyBoolean()))
                .thenReturn(mTestNotificationChannel);

        // Missing Uri permissions for the old channel sound
        final Uri oldSoundUri = Settings.System.DEFAULT_NOTIFICATION_URI;
        doThrow(new SecurityException("no access")).when(mUgmInternal)
                .checkGrantUriPermission(eq(Process.myUid()), any(), eq(oldSoundUri),
                anyInt(), eq(Process.myUserHandle().getIdentifier()));

        // Has Uri permissions for the old channel sound
        final Uri newSoundUri = Uri.parse("content://media/test/sound/uri");
        final NotificationChannel updatedNotificationChannel = new NotificationChannel(
                TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
        updatedNotificationChannel.setSound(newSoundUri,
                updatedNotificationChannel.getAudioAttributes());

        mBinderService.updateNotificationChannelFromPrivilegedListener(
                null, PKG, Process.myUserHandle(), updatedNotificationChannel);

Loading