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 &lt;intent-filter&gt;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