• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &lt;intent-filter&gt;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