1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm; 18 19 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH; 20 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH; 21 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH; 22 import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING; 23 import static com.android.server.pm.PackageManagerService.TAG; 24 25 import android.annotation.Nullable; 26 import android.app.ActivityManager; 27 import android.app.ActivityManagerInternal; 28 import android.compat.annotation.ChangeId; 29 import android.compat.annotation.Disabled; 30 import android.compat.annotation.EnabledAfter; 31 import android.compat.annotation.Overridable; 32 import android.content.Intent; 33 import android.content.IntentFilter; 34 import android.content.pm.ActivityInfo; 35 import android.content.pm.ComponentInfo; 36 import android.content.pm.ResolveInfo; 37 import android.content.pm.ServiceInfo; 38 import android.os.Build; 39 import android.os.Process; 40 import android.os.UserHandle; 41 import android.security.Flags; 42 import android.util.Log; 43 import android.util.LogPrinter; 44 import android.util.Printer; 45 import android.util.Slog; 46 47 import com.android.internal.pm.pkg.component.ParsedMainComponent; 48 import com.android.internal.util.FrameworkStatsLog; 49 import com.android.server.IntentResolver; 50 import com.android.server.LocalServices; 51 import com.android.server.am.BroadcastFilter; 52 import com.android.server.compat.PlatformCompat; 53 import com.android.server.pm.resolution.ComponentResolverApi; 54 import com.android.server.pm.snapshot.PackageDataSnapshot; 55 56 import java.util.List; 57 58 /** 59 * The way Safer Intent is implemented is to add several "hooks" into PMS's intent 60 * resolution process, and in some cases, AMS's runtime receiver resolution. Think of 61 * these methods as resolution "passes", where they post-process the resolved component list. 62 * <p> 63 * Here are the 4 main hooking entry points for each component type: 64 * <ul> 65 * <li>Activity: {@link ComputerEngine#queryIntentActivitiesInternal} or 66 * {@link ResolveIntentHelper#resolveIntentInternal}</li> 67 * <li>Service: {@link Computer#queryIntentServicesInternal}</li> 68 * <li>Static BroadcastReceivers: {@link ResolveIntentHelper#queryIntentReceiversInternal}</li> 69 * <li>Runtime BroadcastReceivers: 70 * {@link com.android.server.am.ActivityManagerService#broadcastIntentLockedTraced}</li> 71 * </ul> 72 */ 73 public class SaferIntentUtils { 74 75 // This is a hack to workaround b/240373119; a proper fix should be implemented instead. 76 public static final ThreadLocal<Boolean> DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 77 ThreadLocal.withInitial(() -> false); 78 79 /** 80 * Apps targeting Android U and above will need to export components in order to invoke them 81 * through implicit intents. 82 * <p> 83 * If a component is not exported and invoked, it will be removed from the list of receivers. 84 * This applies specifically to activities and broadcasts. 85 */ 86 @ChangeId 87 @Overridable 88 @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) 89 private static final long IMPLICIT_INTENTS_ONLY_MATCH_EXPORTED_COMPONENTS = 229362273; 90 91 /** 92 * Intents sent from apps enabling this feature will stop resolving to components with 93 * non matching intent filters, even when explicitly setting a component name, unless the 94 * target components are in the same app as the calling app. 95 * <p> 96 * When an app registers an exported component in its manifest and adds <intent-filter>s, 97 * the component can be started by any intent - even those that do not match the intent filter. 98 * This has proven to be something that many developers find counterintuitive. 99 * Without checking the intent when the component is started, in some circumstances this can 100 * allow 3P apps to trigger internal-only functionality. 101 */ 102 @ChangeId 103 @Overridable 104 @Disabled 105 private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188; 106 107 @Nullable infoToComponent( ComponentInfo info, ComponentResolverApi resolver, boolean isReceiver)108 private static ParsedMainComponent infoToComponent( 109 ComponentInfo info, ComponentResolverApi resolver, boolean isReceiver) { 110 if (info instanceof ActivityInfo) { 111 if (isReceiver) { 112 return resolver.getReceiver(info.getComponentName()); 113 } else { 114 return resolver.getActivity(info.getComponentName()); 115 } 116 } else if (info instanceof ServiceInfo) { 117 return resolver.getService(info.getComponentName()); 118 } else { 119 // This shall never happen 120 throw new IllegalArgumentException("Unsupported component type"); 121 } 122 } 123 124 /** 125 * Helper method to report an unsafe intent event. 126 */ reportUnsafeIntentEvent( int event, int callingUid, int callingPid, Intent intent, String resolvedType, boolean blocked)127 public static void reportUnsafeIntentEvent( 128 int event, int callingUid, int callingPid, 129 Intent intent, String resolvedType, boolean blocked) { 130 String[] categories = intent.getCategories() == null ? new String[0] 131 : intent.getCategories().toArray(String[]::new); 132 String component = intent.getComponent() == null ? null 133 : intent.getComponent().flattenToString(); 134 FrameworkStatsLog.write(FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED, 135 event, 136 callingUid, 137 component, 138 intent.getPackage(), 139 intent.getAction(), 140 categories, 141 resolvedType, 142 intent.getScheme(), 143 blocked); 144 LocalServices.getService(ActivityManagerInternal.class) 145 .triggerUnsafeIntentStrictMode(callingPid, event, intent); 146 } 147 148 /** 149 * All the relevant information about an intent resolution transaction. 150 */ 151 public static class IntentArgs { 152 153 /* Several system_server components */ 154 155 @Nullable 156 public PlatformCompat platformCompat; 157 @Nullable 158 public PackageDataSnapshot snapshot; 159 160 /* Information about the intent itself */ 161 162 public Intent intent; 163 public String resolvedType; 164 public boolean isReceiver; 165 166 /* Information about the caller */ 167 168 // Whether this intent resolution transaction is actually for starting a component and 169 // not only for querying matching components. 170 // This information is required because we only want to log and trigger strict mode 171 // violations on unsafe intent events when the caller actually wants to start something. 172 public boolean resolveForStart; 173 public int callingUid; 174 // When resolveForStart is false, callingPid does not matter as this is only used 175 // to lookup the strict mode violation callback. 176 public int callingPid; 177 IntentArgs( Intent intent, String resolvedType, boolean isReceiver, boolean resolveForStart, int callingUid, int callingPid)178 public IntentArgs( 179 Intent intent, String resolvedType, boolean isReceiver, 180 boolean resolveForStart, int callingUid, int callingPid) { 181 this.isReceiver = isReceiver; 182 this.intent = intent; 183 this.resolvedType = resolvedType; 184 this.resolveForStart = resolveForStart; 185 this.callingUid = callingUid; 186 this.callingPid = resolveForStart ? callingPid : Process.INVALID_PID; 187 } 188 isChangeEnabled(long changeId)189 boolean isChangeEnabled(long changeId) { 190 return platformCompat == null || platformCompat.isChangeEnabledByUidInternalNoLogging( 191 changeId, callingUid); 192 } 193 reportEvent(int event, boolean blocked)194 void reportEvent(int event, boolean blocked) { 195 if (resolveForStart) { 196 SaferIntentUtils.reportUnsafeIntentEvent( 197 event, callingUid, callingPid, intent, resolvedType, blocked); 198 } 199 } 200 } 201 202 /** 203 * Remove components if the intent has null action. 204 * <p> 205 * Because blocking null action applies to all resolution cases, it has to be hooked 206 * in all 4 locations. Note, for component intent resolution in Activity, Service, 207 * and static BroadcastReceivers, null action blocking is actually handled within 208 * {@link #enforceIntentFilterMatching}; we only need to handle it in this method when 209 * the intent does not specify an explicit component name. 210 * <p> 211 * `compat` and `snapshot` may be null when this method is called in ActivityManagerService 212 * CTS tests. The code in this method shall properly avoid control flows using these arguments. 213 */ blockNullAction(IntentArgs args, List componentList)214 public static void blockNullAction(IntentArgs args, List componentList) { 215 if (ActivityManager.canAccessUnexportedComponents(args.callingUid)) return; 216 217 final Computer computer = (Computer) args.snapshot; 218 ComponentResolverApi resolver = null; 219 220 final boolean enforce = Flags.blockNullActionIntents() 221 && args.isChangeEnabled(IntentFilter.BLOCK_NULL_ACTION_INTENTS); 222 223 for (int i = componentList.size() - 1; i >= 0; --i) { 224 boolean match = true; 225 226 Object c = componentList.get(i); 227 if (c instanceof ResolveInfo resolveInfo) { 228 if (computer == null) { 229 // PackageManagerService is not started 230 return; 231 } 232 if (resolver == null) { 233 resolver = computer.getComponentResolver(); 234 } 235 final ParsedMainComponent comp = infoToComponent( 236 resolveInfo.getComponentInfo(), resolver, args.isReceiver); 237 if (comp != null && !comp.getIntents().isEmpty() 238 && args.intent.getAction() == null) { 239 match = false; 240 } 241 } else if (c instanceof IntentFilter) { 242 if (args.intent.getAction() == null) { 243 match = false; 244 } 245 } 246 247 if (!match) { 248 args.reportEvent( 249 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH, enforce); 250 if (enforce) { 251 Slog.w(TAG, "Blocking intent with null action: " + args.intent); 252 componentList.remove(i); 253 } 254 } 255 } 256 } 257 258 /** 259 * Remove ResolveInfos that does not match the provided component intent. 260 * <p> 261 * Component intents cannot refer to a runtime registered BroadcastReceiver, so we only 262 * need to hook into the rest of the 3 entry points. Please note, this method also 263 * handles null action blocking for all component intents; do not go through an additional 264 * {@link #blockNullAction} pass! 265 */ enforceIntentFilterMatching( IntentArgs args, List<ResolveInfo> resolveInfos)266 public static void enforceIntentFilterMatching( 267 IntentArgs args, List<ResolveInfo> resolveInfos) { 268 if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return; 269 270 // Do not enforce filter matching when the caller is system or root 271 if (ActivityManager.canAccessUnexportedComponents(args.callingUid)) return; 272 273 final Computer computer = (Computer) args.snapshot; 274 final ComponentResolverApi resolver = computer.getComponentResolver(); 275 276 final Printer logPrinter = DEBUG_INTENT_MATCHING 277 ? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM) 278 : null; 279 280 final boolean enforceMatch = Flags.enforceIntentFilterMatch() 281 && args.isChangeEnabled(ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS); 282 final boolean blockNullAction = Flags.blockNullActionIntents() 283 && args.isChangeEnabled(IntentFilter.BLOCK_NULL_ACTION_INTENTS); 284 285 for (int i = resolveInfos.size() - 1; i >= 0; --i) { 286 final ComponentInfo info = resolveInfos.get(i).getComponentInfo(); 287 288 // Skip filter matching when the caller is targeting the same app 289 if (UserHandle.isSameApp(args.callingUid, info.applicationInfo.uid)) { 290 continue; 291 } 292 293 final ParsedMainComponent comp = infoToComponent(info, resolver, args.isReceiver); 294 295 if (comp == null || comp.getIntents().isEmpty()) { 296 continue; 297 } 298 299 Boolean match = null; 300 301 if (args.intent.getAction() == null) { 302 args.reportEvent( 303 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH, 304 enforceMatch && blockNullAction); 305 if (blockNullAction) { 306 // Skip intent filter matching if blocking null action 307 match = false; 308 } 309 } 310 311 if (match == null) { 312 // Check if any intent filter matches 313 for (int j = 0, size = comp.getIntents().size(); j < size; ++j) { 314 IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter(); 315 if (IntentResolver.intentMatchesFilter( 316 intentFilter, args.intent, args.resolvedType)) { 317 match = true; 318 break; 319 } 320 } 321 } 322 323 // At this point, the value `match` has the following states: 324 // null : Intent does not match any intent filter 325 // false: Null action intent detected AND blockNullAction == true 326 // true : The intent matches at least one intent filter 327 328 if (match == null) { 329 args.reportEvent( 330 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH, 331 enforceMatch); 332 match = false; 333 } 334 335 if (!match) { 336 // All non-matching intents has to be marked accordingly 337 if (Flags.enforceIntentFilterMatch()) { 338 args.intent.addExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); 339 } 340 if (enforceMatch) { 341 Slog.w(TAG, "Intent does not match component's intent filter: " + args.intent); 342 Slog.w(TAG, "Access blocked: " + comp.getComponentName()); 343 if (DEBUG_INTENT_MATCHING) { 344 Slog.v(TAG, "Component intent filters:"); 345 comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " ")); 346 Slog.v(TAG, "-----------------------------"); 347 } 348 resolveInfos.remove(i); 349 } 350 } 351 } 352 } 353 354 /** 355 * Filter non-exported components from the componentList if intent is implicit. 356 * <p> 357 * Implicit intents cannot be used to start Services since API 21+. 358 * Implicit broadcasts cannot be delivered to static BroadcastReceivers since API 25+. 359 * So we only need to hook into Activity and runtime BroadcastReceiver intent resolution. 360 */ filterNonExportedComponents(IntentArgs args, List componentList)361 public static void filterNonExportedComponents(IntentArgs args, List componentList) { 362 if (componentList == null 363 || args.intent.getPackage() != null 364 || args.intent.getComponent() != null 365 || ActivityManager.canAccessUnexportedComponents(args.callingUid)) { 366 return; 367 } 368 369 final boolean enforce = 370 args.isChangeEnabled(IMPLICIT_INTENTS_ONLY_MATCH_EXPORTED_COMPONENTS); 371 boolean violated = false; 372 373 for (int i = componentList.size() - 1; i >= 0; i--) { 374 Object c = componentList.get(i); 375 if (c instanceof ResolveInfo resolveInfo) { 376 if (resolveInfo.getComponentInfo().exported) { 377 continue; 378 } 379 } else if (c instanceof BroadcastFilter broadcastFilter) { 380 if (broadcastFilter.exported) { 381 continue; 382 } 383 } else { 384 continue; 385 } 386 violated = true; 387 if (!enforce) { 388 break; 389 } 390 componentList.remove(i); 391 } 392 393 if (violated) { 394 args.reportEvent( 395 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH, 396 enforce); 397 } 398 } 399 } 400