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.pm.pkg.component.ParsedMainComponentImpl; 49 import com.android.internal.util.FrameworkStatsLog; 50 import com.android.server.IntentResolver; 51 import com.android.server.LocalServices; 52 import com.android.server.am.BroadcastFilter; 53 import com.android.server.compat.PlatformCompat; 54 import com.android.server.pm.resolution.ComponentResolverApi; 55 import com.android.server.pm.snapshot.PackageDataSnapshot; 56 57 import java.util.List; 58 59 /** 60 * The way Safer Intent is implemented is to add several "hooks" into PMS's intent 61 * resolution process, and in some cases, AMS's runtime receiver resolution. Think of 62 * these methods as resolution "passes", where they post-process the resolved component list. 63 * <p> 64 * Here are the 4 main hooking entry points for each component type: 65 * <ul> 66 * <li>Activity: {@link ComputerEngine#queryIntentActivitiesInternal} or 67 * {@link ResolveIntentHelper#resolveIntentInternal}</li> 68 * <li>Service: {@link Computer#queryIntentServicesInternal}</li> 69 * <li>Static BroadcastReceivers: {@link ResolveIntentHelper#queryIntentReceiversInternal}</li> 70 * <li>Runtime BroadcastReceivers: 71 * {@link com.android.server.am.ActivityManagerService#broadcastIntentLockedTraced}</li> 72 * </ul> 73 */ 74 public class SaferIntentUtils { 75 76 // This is a hack to workaround b/240373119; a proper fix should be implemented instead. 77 public static final ThreadLocal<Boolean> DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 78 ThreadLocal.withInitial(() -> false); 79 80 /** 81 * Apps targeting Android U and above will need to export components in order to invoke them 82 * through implicit intents. 83 * <p> 84 * If a component is not exported and invoked, it will be removed from the list of receivers. 85 * This applies specifically to activities and broadcasts. 86 */ 87 @ChangeId 88 @Overridable 89 @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) 90 private static final long IMPLICIT_INTENTS_ONLY_MATCH_EXPORTED_COMPONENTS = 229362273; 91 92 /** 93 * Intents sent from apps enabling this feature will stop resolving to components with 94 * non matching intent filters, even when explicitly setting a component name, unless the 95 * target components are in the same app as the calling app. 96 * <p> 97 * When an app registers an exported component in its manifest and adds <intent-filter>s, 98 * the component can be started by any intent - even those that do not match the intent filter. 99 * This has proven to be something that many developers find counterintuitive. 100 * Without checking the intent when the component is started, in some circumstances this can 101 * allow 3P apps to trigger internal-only functionality. 102 */ 103 @ChangeId 104 @Overridable 105 @Disabled 106 private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188; 107 108 @Nullable infoToComponent( ComponentInfo info, ComponentResolverApi resolver, boolean isReceiver)109 private static ParsedMainComponent infoToComponent( 110 ComponentInfo info, ComponentResolverApi resolver, boolean isReceiver) { 111 if (info instanceof ActivityInfo) { 112 if (isReceiver) { 113 return resolver.getReceiver(info.getComponentName()); 114 } else { 115 return resolver.getActivity(info.getComponentName()); 116 } 117 } else if (info instanceof ServiceInfo) { 118 return resolver.getService(info.getComponentName()); 119 } else { 120 // This shall never happen 121 throw new IllegalArgumentException("Unsupported component type"); 122 } 123 } 124 125 /** 126 * Helper method to report an unsafe intent event. 127 */ reportUnsafeIntentEvent( int event, int callingUid, int callingPid, Intent intent, String resolvedType, boolean blocked)128 public static void reportUnsafeIntentEvent( 129 int event, int callingUid, int callingPid, 130 Intent intent, String resolvedType, boolean blocked) { 131 String[] categories = intent.getCategories() == null ? new String[0] 132 : intent.getCategories().toArray(String[]::new); 133 String component = intent.getComponent() == null ? null 134 : intent.getComponent().flattenToString(); 135 FrameworkStatsLog.write(FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED, 136 event, 137 callingUid, 138 component, 139 intent.getPackage(), 140 intent.getAction(), 141 categories, 142 resolvedType, 143 intent.getScheme(), 144 blocked); 145 LocalServices.getService(ActivityManagerInternal.class) 146 .triggerUnsafeIntentStrictMode(callingPid, event, intent); 147 } 148 149 /** 150 * All the relevant information about an intent resolution transaction. 151 */ 152 public static class IntentArgs { 153 154 /* Several system_server components */ 155 156 @Nullable 157 public PlatformCompat platformCompat; 158 @Nullable 159 public PackageDataSnapshot snapshot; 160 161 /* Information about the intent itself */ 162 163 public Intent intent; 164 public String resolvedType; 165 public boolean isReceiver; 166 167 /* Information about the caller */ 168 169 // Whether this intent resolution transaction is actually for starting a component and 170 // not only for querying matching components. 171 // This information is required because we only want to log and trigger strict mode 172 // violations on unsafe intent events when the caller actually wants to start something. 173 public boolean resolveForStart; 174 public int callingUid; 175 // When resolveForStart is false, callingPid does not matter as this is only used 176 // to lookup the strict mode violation callback. 177 public int callingPid; 178 IntentArgs( Intent intent, String resolvedType, boolean isReceiver, boolean resolveForStart, int callingUid, int callingPid)179 public IntentArgs( 180 Intent intent, String resolvedType, boolean isReceiver, 181 boolean resolveForStart, int callingUid, int callingPid) { 182 this.isReceiver = isReceiver; 183 this.intent = intent; 184 this.resolvedType = resolvedType; 185 this.resolveForStart = resolveForStart; 186 this.callingUid = callingUid; 187 this.callingPid = resolveForStart ? callingPid : Process.INVALID_PID; 188 } 189 isChangeEnabled(long changeId)190 boolean isChangeEnabled(long changeId) { 191 return platformCompat == null || platformCompat.isChangeEnabledByUidInternalNoLogging( 192 changeId, callingUid); 193 } 194 reportEvent(int event, boolean blocked)195 void reportEvent(int event, boolean blocked) { 196 if (resolveForStart) { 197 SaferIntentUtils.reportUnsafeIntentEvent( 198 event, callingUid, callingPid, intent, resolvedType, blocked); 199 } 200 } 201 } 202 203 /** 204 * Remove components if the intent has null action. 205 * <p> 206 * Because blocking null action applies to all resolution cases, it has to be hooked 207 * in all 4 locations. Note, for component intent resolution in Activity, Service, 208 * and static BroadcastReceivers, null action blocking is actually handled within 209 * {@link #enforceIntentFilterMatching}; we only need to handle it in this method when 210 * the intent does not specify an explicit component name. 211 * <p> 212 * `compat` and `snapshot` may be null when this method is called in ActivityManagerService 213 * CTS tests. The code in this method shall properly avoid control flows using these arguments. 214 */ blockNullAction(IntentArgs args, List componentList)215 public static void blockNullAction(IntentArgs args, List componentList) { 216 if (args.intent.getAction() != null) return; 217 if (ActivityManager.canAccessUnexportedComponents(args.callingUid)) return; 218 219 final Computer computer = (Computer) args.snapshot; 220 ComponentResolverApi resolver = null; 221 222 final boolean enforce = Flags.blockNullActionIntents() 223 && args.isChangeEnabled(IntentFilter.BLOCK_NULL_ACTION_INTENTS); 224 225 for (int i = componentList.size() - 1; i >= 0; --i) { 226 boolean match = true; 227 228 Object c = componentList.get(i); 229 if (c instanceof ResolveInfo resolveInfo) { 230 if (computer == null) { 231 // PackageManagerService is not started 232 return; 233 } 234 if (resolver == null) { 235 resolver = computer.getComponentResolver(); 236 } 237 final ParsedMainComponent comp = infoToComponent( 238 resolveInfo.getComponentInfo(), resolver, args.isReceiver); 239 if (comp != null && !comp.getIntents().isEmpty()) { 240 match = false; 241 } 242 } else if (c instanceof IntentFilter) { 243 match = false; 244 } 245 246 if (!match) { 247 args.reportEvent( 248 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH, enforce); 249 if (enforce) { 250 Slog.w(TAG, "Blocking intent with null action: " + args.intent); 251 componentList.remove(i); 252 } 253 } 254 } 255 } 256 257 /** 258 * Remove ResolveInfos that does not match the provided component intent. 259 * <p> 260 * Component intents cannot refer to a runtime registered BroadcastReceiver, so we only 261 * need to hook into the rest of the 3 entry points. Please note, this method also 262 * handles null action blocking for all component intents; do not go through an additional 263 * {@link #blockNullAction} pass! 264 */ enforceIntentFilterMatching( IntentArgs args, List<ResolveInfo> resolveInfos)265 public static void enforceIntentFilterMatching( 266 IntentArgs args, List<ResolveInfo> resolveInfos) { 267 // Switch to the new intent matching logic if the feature flag is enabled. 268 // Otherwise, use the existing AppCompat based implementation. 269 if (Flags.enableIntentMatchingFlags()) { 270 enforceIntentFilterMatchingWithIntentMatchingFlags(args, resolveInfos); 271 } else { 272 enforceIntentFilterMatchingWithAppCompat(args, resolveInfos); 273 } 274 } 275 276 /** 277 * This version of the method is implemented in Android B and uses "IntentMatchingFlags" 278 */ enforceIntentFilterMatchingWithIntentMatchingFlags( IntentArgs args, List<ResolveInfo> resolveInfos)279 private static void enforceIntentFilterMatchingWithIntentMatchingFlags( 280 IntentArgs args, List<ResolveInfo> resolveInfos) { 281 if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return; 282 283 // Do not enforce filter matching when the caller is system or root 284 if (ActivityManager.canAccessUnexportedComponents(args.callingUid)) return; 285 286 final Computer computer = (Computer) args.snapshot; 287 final ComponentResolverApi resolver = computer.getComponentResolver(); 288 289 final Printer logPrinter = DEBUG_INTENT_MATCHING 290 ? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM) 291 : null; 292 293 for (int i = resolveInfos.size() - 1; i >= 0; --i) { 294 final ComponentInfo info = resolveInfos.get(i).getComponentInfo(); 295 296 // Skip filter matching when the caller is targeting the same app 297 if (UserHandle.isSameApp(args.callingUid, info.applicationInfo.uid)) { 298 continue; 299 } 300 301 final ParsedMainComponent comp = infoToComponent(info, resolver, args.isReceiver); 302 303 if (comp == null || comp.getIntents().isEmpty()) { 304 continue; 305 } 306 307 boolean enforceIntentFilter = Flags.enableIntentMatchingFlags(); 308 boolean allowNullAction = false; 309 310 if (Flags.enableIntentMatchingFlags()) { 311 int flags = comp.getIntentMatchingFlags(); 312 if (flags == 0 || (flags & ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_NONE) 313 == ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_NONE 314 || (flags 315 & ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER) 316 == 0) { 317 enforceIntentFilter = false; 318 } 319 if ((flags & ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION) 320 == ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION) { 321 allowNullAction = true; 322 } 323 } 324 325 boolean hasNullAction = args.intent.getAction() == null; 326 boolean intentMatchesComponent = false; 327 328 for (int j = 0, size = comp.getIntents().size(); j < size; ++j) { 329 IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter(); 330 if (IntentResolver.intentMatchesFilter( 331 intentFilter, args.intent, args.resolvedType)) { 332 intentMatchesComponent = true; 333 break; 334 } 335 } 336 337 boolean blockIntent = false; 338 if (enforceIntentFilter) { 339 if ((hasNullAction && !allowNullAction) || !intentMatchesComponent) { 340 blockIntent = true; 341 } 342 } 343 344 if (hasNullAction) { 345 args.reportEvent( 346 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH, blockIntent); 347 } else if (!intentMatchesComponent) { 348 args.reportEvent( 349 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH, 350 blockIntent); 351 } 352 353 if (Flags.enforceIntentFilterMatch() && (hasNullAction || !intentMatchesComponent)) { 354 args.intent.addExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); 355 } 356 357 if (blockIntent) { 358 Slog.w(TAG, "Intent does not match component's intent filter: " + args.intent); 359 Slog.w(TAG, "Access blocked: " + comp.getComponentName()); 360 if (DEBUG_INTENT_MATCHING) { 361 Slog.v(TAG, "Component intent filters:"); 362 comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " ")); 363 Slog.v(TAG, "-----------------------------"); 364 } 365 resolveInfos.remove(i); 366 } 367 } 368 } 369 370 /** 371 * This version of the method is implemented in Android V and uses "AppCompat" 372 */ enforceIntentFilterMatchingWithAppCompat( IntentArgs args, List<ResolveInfo> resolveInfos)373 private static void enforceIntentFilterMatchingWithAppCompat( 374 IntentArgs args, List<ResolveInfo> resolveInfos) { 375 if (DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.get()) return; 376 377 // Do not enforce filter matching when the caller is system or root 378 if (ActivityManager.canAccessUnexportedComponents(args.callingUid)) return; 379 380 final Computer computer = (Computer) args.snapshot; 381 final ComponentResolverApi resolver = computer.getComponentResolver(); 382 383 final Printer logPrinter = DEBUG_INTENT_MATCHING 384 ? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM) 385 : null; 386 387 final boolean enforceMatch = Flags.enforceIntentFilterMatch() 388 && args.isChangeEnabled(ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS); 389 final boolean blockNullAction = Flags.blockNullActionIntents() 390 && args.isChangeEnabled(IntentFilter.BLOCK_NULL_ACTION_INTENTS); 391 392 for (int i = resolveInfos.size() - 1; i >= 0; --i) { 393 final ComponentInfo info = resolveInfos.get(i).getComponentInfo(); 394 395 // Skip filter matching when the caller is targeting the same app 396 if (UserHandle.isSameApp(args.callingUid, info.applicationInfo.uid)) { 397 continue; 398 } 399 400 final ParsedMainComponent comp = infoToComponent(info, resolver, args.isReceiver); 401 402 if (comp == null || comp.getIntents().isEmpty()) { 403 continue; 404 } 405 406 Boolean match = null; 407 408 if (args.intent.getAction() == null) { 409 args.reportEvent( 410 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH, 411 enforceMatch && blockNullAction); 412 if (blockNullAction) { 413 // Skip intent filter matching if blocking null action 414 match = false; 415 } 416 } 417 418 if (match == null) { 419 // Check if any intent filter matches 420 for (int j = 0, size = comp.getIntents().size(); j < size; ++j) { 421 IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter(); 422 if (IntentResolver.intentMatchesFilter( 423 intentFilter, args.intent, args.resolvedType)) { 424 match = true; 425 break; 426 } 427 } 428 } 429 430 // At this point, the value `match` has the following states: 431 // null : Intent does not match any intent filter 432 // false: Null action intent detected AND blockNullAction == true 433 // true : The intent matches at least one intent filter 434 435 if (match == null) { 436 args.reportEvent( 437 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH, 438 enforceMatch); 439 match = false; 440 } 441 442 if (!match) { 443 // All non-matching intents has to be marked accordingly 444 if (Flags.enforceIntentFilterMatch()) { 445 args.intent.addExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); 446 } 447 if (enforceMatch) { 448 Slog.w(TAG, "Intent does not match component's intent filter: " + args.intent); 449 Slog.w(TAG, "Access blocked: " + comp.getComponentName()); 450 if (DEBUG_INTENT_MATCHING) { 451 Slog.v(TAG, "Component intent filters:"); 452 comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " ")); 453 Slog.v(TAG, "-----------------------------"); 454 } 455 resolveInfos.remove(i); 456 } 457 } 458 } 459 } 460 461 /** 462 * Filter non-exported components from the componentList if intent is implicit. 463 * <p> 464 * Implicit intents cannot be used to start Services since API 21+. 465 * Implicit broadcasts cannot be delivered to static BroadcastReceivers since API 25+. 466 * So we only need to hook into Activity and runtime BroadcastReceiver intent resolution. 467 */ filterNonExportedComponents(IntentArgs args, List componentList)468 public static void filterNonExportedComponents(IntentArgs args, List componentList) { 469 if (componentList == null 470 || args.intent.getPackage() != null 471 || args.intent.getComponent() != null 472 || ActivityManager.canAccessUnexportedComponents(args.callingUid)) { 473 return; 474 } 475 476 final boolean enforce = 477 args.isChangeEnabled(IMPLICIT_INTENTS_ONLY_MATCH_EXPORTED_COMPONENTS); 478 boolean violated = false; 479 480 for (int i = componentList.size() - 1; i >= 0; i--) { 481 Object c = componentList.get(i); 482 if (c instanceof ResolveInfo resolveInfo) { 483 if (resolveInfo.getComponentInfo().exported) { 484 continue; 485 } 486 } else if (c instanceof BroadcastFilter broadcastFilter) { 487 if (broadcastFilter.exported) { 488 continue; 489 } 490 } else { 491 continue; 492 } 493 violated = true; 494 if (!enforce) { 495 break; 496 } 497 componentList.remove(i); 498 } 499 500 if (violated) { 501 args.reportEvent( 502 UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH, 503 enforce); 504 } 505 } 506 } 507