1 /*
<lambda>null2  * Copyright (C) 2022 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.google.android.lint
18 
19 import com.android.tools.lint.client.api.UastParser
20 import com.android.tools.lint.detector.api.Category
21 import com.android.tools.lint.detector.api.Context
22 import com.android.tools.lint.detector.api.Detector
23 import com.android.tools.lint.detector.api.Implementation
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.Scope
26 import com.android.tools.lint.detector.api.Severity
27 import com.android.tools.lint.detector.api.SourceCodeScanner
28 import com.android.tools.lint.detector.api.interprocedural.CallGraph
29 import com.android.tools.lint.detector.api.interprocedural.CallGraphResult
30 import com.android.tools.lint.detector.api.interprocedural.searchForPaths
31 import com.intellij.psi.PsiAnonymousClass
32 import com.intellij.psi.PsiMethod
33 import java.util.LinkedList
34 import org.jetbrains.uast.UCallExpression
35 import org.jetbrains.uast.UElement
36 import org.jetbrains.uast.UMethod
37 import org.jetbrains.uast.UParameter
38 import org.jetbrains.uast.USimpleNameReferenceExpression
39 import org.jetbrains.uast.visitor.AbstractUastVisitor
40 
41 /**
42  * A lint checker to detect potential package visibility issues for system's APIs. APIs working
43  * in the system_server and taking the package name as a parameter may have chance to reveal
44  * package existence status on the device, and break the
45  * <a href="https://developer.android.com/about/versions/11/privacy/package-visibility">
46  * Package Visibility</a> that we introduced in Android 11.
47  * <p>
48  * Take an example of the API `boolean setFoo(String packageName)`, a malicious app may have chance
49  * to detect package existence state on the device from the result of the API, if there is no
50  * package visibility filtering rule or uid identify checks applying to the parameter of the
51  * package name.
52  */
53 class PackageVisibilityDetector : Detector(), SourceCodeScanner {
54 
55     // Enables call graph analysis
56     override fun isCallGraphRequired(): Boolean = true
57 
58     override fun analyzeCallGraph(
59         context: Context,
60         callGraph: CallGraphResult
61     ) {
62         val systemServerApiNodes = callGraph.callGraph.nodes.filter(::isSystemServerApi)
63         val sinkMethodNodes = callGraph.callGraph.nodes.filter {
64             // TODO(b/228285232): Remove enforce permission sink methods
65             isNodeInList(it, ENFORCE_PERMISSION_METHODS) || isNodeInList(it, APPOPS_METHODS)
66         }
67         val parser = context.client.getUastParser(context.project)
68         analyzeApisContainPackageNameParameters(
69             context, parser, systemServerApiNodes, sinkMethodNodes)
70     }
71 
72     /**
73      * Looking for API contains package name parameters, report the lint issue if the API does not
74      * invoke any sink methods.
75      */
76     private fun analyzeApisContainPackageNameParameters(
77         context: Context,
78         parser: UastParser,
79         systemServerApiNodes: List<CallGraph.Node>,
80         sinkMethodNodes: List<CallGraph.Node>
81     ) {
82         for (apiNode in systemServerApiNodes) {
83             val apiMethod = apiNode.getUMethod() ?: continue
84             val pkgNameParamIndexes = apiMethod.uastParameters.mapIndexedNotNull { index, param ->
85                 if (Parameter(param) in PACKAGE_NAME_PATTERNS && apiNode.isArgumentInUse(index)) {
86                     index
87                 } else {
88                     null
89                 }
90             }.takeIf(List<Int>::isNotEmpty) ?: continue
91 
92             for (pkgNameParamIndex in pkgNameParamIndexes) {
93                 // Trace the call path of the method's argument, pass the lint checks if a sink
94                 // method is found
95                 if (traceArgumentCallPath(
96                         apiNode, pkgNameParamIndex, PACKAGE_NAME_SINK_METHOD_LIST)) {
97                     continue
98                 }
99                 // Pass the check if one of the sink methods is invoked
100                 if (hasValidPath(
101                         searchForPaths(
102                             sources = listOf(apiNode),
103                             isSink = { it in sinkMethodNodes },
104                             getNeighbors = { node -> node.edges.map { it.node!! } }
105                         )
106                     )
107                 ) continue
108 
109                 // Report issue
110                 val reportElement = apiMethod.uastParameters[pkgNameParamIndex] as UElement
111                 val location = parser.createLocation(reportElement)
112                 context.report(
113                     ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
114                     location,
115                     getMsgPackageNameNoPackageVisibilityFilters(apiMethod, pkgNameParamIndex)
116                 )
117             }
118         }
119     }
120 
121     /**
122      * Returns {@code true} if the method associated with the given node is a system server's
123      * public API that extends from Stub class.
124      */
125     private fun isSystemServerApi(
126         node: CallGraph.Node
127     ): Boolean {
128         val method = node.getUMethod() ?: return false
129         if (!method.hasModifierProperty("public") ||
130             method.uastBody == null ||
131             method.containingClass is PsiAnonymousClass) {
132             return false
133         }
134         val className = method.containingClass?.qualifiedName ?: return false
135         if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
136             return false
137         }
138         return (method.containingClass ?: return false).supers
139             .filter { it.name == CLASS_STUB }
140             .filter { it.qualifiedName !in BYPASS_STUBS }
141             .any { it.findMethodBySignature(method, /* checkBases */ true) != null }
142     }
143 
144     /**
145      * Returns {@code true} if the list contains the node of the call graph.
146      */
147     private fun isNodeInList(
148         node: CallGraph.Node,
149         filters: List<Method>
150     ): Boolean {
151         val method = node.getUMethod() ?: return false
152         return Method(method) in filters
153     }
154 
155     /**
156      * Trace the call paths of the argument of the method in the start entry. Return {@code true}
157      * if one of methods in the sink call list is invoked.
158      * Take an example of the call path:
159      * foo(packageName) -> a(packageName) -> b(packageName) -> filterAppAccess()
160      * It returns {@code true} if the filterAppAccess() is in the sink call list.
161      */
162     private fun traceArgumentCallPath(
163         apiNode: CallGraph.Node,
164         pkgNameParamIndex: Int,
165         sinkList: List<Method>
166     ): Boolean {
167         val startEntry = TraceEntry(apiNode, pkgNameParamIndex)
168         val traceQueue = LinkedList<TraceEntry>().apply { add(startEntry) }
169         val allVisits = mutableSetOf<TraceEntry>().apply { add(startEntry) }
170         while (!traceQueue.isEmpty()) {
171             val entry = traceQueue.poll()
172             val entryNode = entry.node
173             val entryMethod = entryNode.getUMethod() ?: continue
174             val entryArgumentName = entryMethod.uastParameters[entry.argumentIndex].name
175             for (outEdge in entryNode.edges) {
176                 val outNode = outEdge.node ?: continue
177                 val outMethod = outNode.getUMethod() ?: continue
178                 val outArgumentIndex =
179                     outEdge.call?.findArgumentIndex(
180                         entryArgumentName, outMethod.uastParameters.size)
181                 val sinkMethod = findInSinkList(outMethod, sinkList)
182                 if (sinkMethod == null) {
183                     if (outArgumentIndex == null) {
184                         // Path is not relevant to the sink method and argument
185                         continue
186                     }
187                     // Path is relevant to the argument, add a new trace entry if never visit before
188                     val newEntry = TraceEntry(outNode, outArgumentIndex)
189                     if (newEntry !in allVisits) {
190                         traceQueue.add(newEntry)
191                         allVisits.add(newEntry)
192                     }
193                     continue
194                 }
195                 if (sinkMethod.matchArgument && outArgumentIndex == null) {
196                     // The sink call is required to match the argument, but not found
197                     continue
198                 }
199                 if (sinkMethod.checkCaller &&
200                     entryMethod.isInClearCallingIdentityScope(outEdge.call!!)) {
201                     // The sink call is in the scope of Binder.clearCallingIdentify
202                     continue
203                 }
204                 // A sink method is matched
205                 return true
206             }
207         }
208         return false
209     }
210 
211     /**
212      * Returns the UMethod associated with the given node of call graph.
213      */
214     private fun CallGraph.Node.getUMethod(): UMethod? = this.target.element as? UMethod
215 
216     /**
217      * Returns the system module name (e.g. com.android.server.pm) of the method of the
218      * call graph node.
219      */
220     private fun CallGraph.Node.getModuleName(): String? {
221         val method = getUMethod() ?: return null
222         val className = method.containingClass?.qualifiedName ?: return null
223         if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
224             return null
225         }
226         val dotPos = className.indexOf(".", SYSTEM_PACKAGE_PREFIX.length)
227         if (dotPos == -1) {
228             return SYSTEM_PACKAGE_PREFIX
229         }
230         return className.substring(0, dotPos)
231     }
232 
233     /**
234      * Return {@code true} if the argument in the method's body is in-use.
235      */
236     private fun CallGraph.Node.isArgumentInUse(argIndex: Int): Boolean {
237         val method = getUMethod() ?: return false
238         val argumentName = method.uastParameters[argIndex].name
239         var foundArg = false
240         val methodVisitor = object : AbstractUastVisitor() {
241             override fun visitSimpleNameReferenceExpression(
242                 node: USimpleNameReferenceExpression
243             ): Boolean {
244                 if (node.identifier == argumentName) {
245                     foundArg = true
246                 }
247                 return true
248             }
249         }
250         method.uastBody?.accept(methodVisitor)
251         return foundArg
252     }
253 
254     /**
255      * Given an argument name, returns the index of argument in the call expression.
256      */
257     private fun UCallExpression.findArgumentIndex(
258         argumentName: String,
259         parameterSize: Int
260     ): Int? {
261         if (valueArgumentCount == 0 || parameterSize == 0) {
262             return null
263         }
264         var match = false
265         val argVisitor = object : AbstractUastVisitor() {
266             override fun visitSimpleNameReferenceExpression(
267                 node: USimpleNameReferenceExpression
268             ): Boolean {
269                 if (node.identifier == argumentName) {
270                     match = true
271                 }
272                 return true
273             }
274             override fun visitCallExpression(node: UCallExpression): Boolean {
275                 return true
276             }
277         }
278         valueArguments.take(parameterSize).forEachIndexed { index, argument ->
279             argument.accept(argVisitor)
280             if (match) {
281                 return index
282             }
283         }
284         return null
285     }
286 
287     /**
288      * Given a UMethod, returns a method from the sink method list.
289      */
290     private fun findInSinkList(
291         uMethod: UMethod,
292         sinkCallList: List<Method>
293     ): Method? {
294         return sinkCallList.find {
295             it == Method(uMethod) ||
296                     it == Method(uMethod.containingClass?.qualifiedName ?: "", "*")
297         }
298     }
299 
300     /**
301      * Returns {@code true} if the call expression is in the scope of the
302      * Binder.clearCallingIdentify.
303      */
304     private fun UMethod.isInClearCallingIdentityScope(call: UCallExpression): Boolean {
305         var isInScope = false
306         val methodVisitor = object : AbstractUastVisitor() {
307             private var clearCallingIdentity = 0
308             override fun visitCallExpression(node: UCallExpression): Boolean {
309                 if (call == node && clearCallingIdentity != 0) {
310                     isInScope = true
311                     return true
312                 }
313                 val visitMethod = Method(node.resolve() ?: return false)
314                 if (visitMethod == METHOD_CLEAR_CALLING_IDENTITY) {
315                     clearCallingIdentity++
316                 } else if (visitMethod == METHOD_RESTORE_CALLING_IDENTITY) {
317                     clearCallingIdentity--
318                 }
319                 return false
320             }
321         }
322         accept(methodVisitor)
323         return isInScope
324     }
325 
326     /**
327      * Checks the module name of the start node and the last node that invokes the sink method
328      * (e.g. checkPermission) in a path, returns {@code true} if one of the paths has the same
329      * module name for both nodes.
330      */
331     private fun hasValidPath(paths: Collection<List<CallGraph.Node>>): Boolean {
332         for (pathNodes in paths) {
333             if (pathNodes.size < VALID_CALL_PATH_NODES_SIZE) {
334                 continue
335             }
336             val startModule = pathNodes[0].getModuleName() ?: continue
337             val lastCallModule = pathNodes[pathNodes.size - 2].getModuleName() ?: continue
338             if (startModule == lastCallModule) {
339                 return true
340             }
341         }
342         return false
343     }
344 
345     /**
346      * A data class to represent the method.
347      */
348     private data class Method(
349         val clazz: String,
350         val name: String
351     ) {
352         // Used by traceArgumentCallPath to indicate that the method is required to match the
353         // argument name
354         var matchArgument = true
355 
356         // Used by traceArgumentCallPath to indicate that the method is required to check whether
357         // the Binder.clearCallingIdentity is invoked.
358         var checkCaller = false
359 
360         constructor(
361             clazz: String,
362             name: String,
363             matchArgument: Boolean = true,
364             checkCaller: Boolean = false
365         ) : this(clazz, name) {
366             this.matchArgument = matchArgument
367             this.checkCaller = checkCaller
368         }
369 
370         constructor(
371             method: PsiMethod
372         ) : this(method.containingClass?.qualifiedName ?: "", method.name)
373 
374         constructor(
375             method: com.google.android.lint.model.Method
376         ) : this(method.clazz, method.name)
377     }
378 
379     /**
380      * A data class to represent the parameter of the method. The parameter name is converted to
381      * lower case letters for comparison.
382      */
383     private data class Parameter private constructor(
384         val typeName: String,
385         val parameterName: String
386     ) {
387         constructor(uParameter: UParameter) : this(
388             uParameter.type.canonicalText,
389             uParameter.name.lowercase()
390         )
391 
392         companion object {
393             fun create(typeName: String, parameterName: String) =
394                 Parameter(typeName, parameterName.lowercase())
395         }
396     }
397 
398     /**
399      * A data class wraps a method node of the call graph and an index that indicates an
400      * argument of the method to record call trace information.
401      */
402     private data class TraceEntry(
403         val node: CallGraph.Node,
404         val argumentIndex: Int
405     )
406 
407     companion object {
408         private const val SYSTEM_PACKAGE_PREFIX = "com.android.server."
409         // A valid call path list needs to contain a start node and a sink node
410         private const val VALID_CALL_PATH_NODES_SIZE = 2
411 
412         private const val CLASS_STRING = "java.lang.String"
413         private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager"
414         private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager"
415         private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager"
416         private const val CLASS_BINDER = "android.os.Binder"
417         private const val CLASS_PACKAGE_MANAGER_INTERNAL =
418             "android.content.pm.PackageManagerInternal"
419 
420         // Patterns of package name parameter
421         private val PACKAGE_NAME_PATTERNS = setOf(
422             Parameter.create(CLASS_STRING, "packageName"),
423             Parameter.create(CLASS_STRING, "callingPackage"),
424             Parameter.create(CLASS_STRING, "callingPackageName"),
425             Parameter.create(CLASS_STRING, "pkgName"),
426             Parameter.create(CLASS_STRING, "callingPkg"),
427             Parameter.create(CLASS_STRING, "pkg")
428         )
429 
430         // Package manager APIs
431         private val PACKAGE_NAME_SINK_METHOD_LIST = listOf(
432             Method(CLASS_PACKAGE_MANAGER_INTERNAL, "filterAppAccess", matchArgument = false),
433             Method(CLASS_PACKAGE_MANAGER_INTERNAL, "canQueryPackage"),
434             Method(CLASS_PACKAGE_MANAGER_INTERNAL, "isSameApp"),
435             Method(CLASS_PACKAGE_MANAGER, "*", checkCaller = true),
436             Method(CLASS_IPACKAGE_MANAGER, "*", checkCaller = true),
437             Method(CLASS_PACKAGE_MANAGER, "getPackagesForUid", matchArgument = false),
438             Method(CLASS_IPACKAGE_MANAGER, "getPackagesForUid", matchArgument = false)
439         )
440 
441         // AppOps APIs which include uid and package visibility filters checks
442         private val APPOPS_METHODS = listOf(
443             Method(CLASS_APPOPS_MANAGER, "noteOp"),
444             Method(CLASS_APPOPS_MANAGER, "noteOpNoThrow"),
445             Method(CLASS_APPOPS_MANAGER, "noteOperation"),
446             Method(CLASS_APPOPS_MANAGER, "noteProxyOp"),
447             Method(CLASS_APPOPS_MANAGER, "noteProxyOpNoThrow"),
448             Method(CLASS_APPOPS_MANAGER, "startOp"),
449             Method(CLASS_APPOPS_MANAGER, "startOpNoThrow"),
450             Method(CLASS_APPOPS_MANAGER, "FinishOp"),
451             Method(CLASS_APPOPS_MANAGER, "finishProxyOp"),
452             Method(CLASS_APPOPS_MANAGER, "checkPackage")
453         )
454 
455         // Enforce permission APIs
456         private val ENFORCE_PERMISSION_METHODS =
457                 com.google.android.lint.ENFORCE_PERMISSION_METHODS
458                         .map(PackageVisibilityDetector::Method)
459 
460         private val BYPASS_STUBS = listOf(
461             "android.content.pm.IPackageDataObserver.Stub",
462             "android.content.pm.IPackageDeleteObserver.Stub",
463             "android.content.pm.IPackageDeleteObserver2.Stub",
464             "android.content.pm.IPackageInstallObserver2.Stub",
465             "com.android.internal.app.IAppOpsCallback.Stub",
466 
467             // TODO(b/228285637): Do not bypass PackageManagerService API
468             "android.content.pm.IPackageManager.Stub",
469             "android.content.pm.IPackageManagerNative.Stub"
470         )
471 
472         private val METHOD_CLEAR_CALLING_IDENTITY =
473             Method(CLASS_BINDER, "clearCallingIdentity")
474         private val METHOD_RESTORE_CALLING_IDENTITY =
475             Method(CLASS_BINDER, "restoreCallingIdentity")
476 
477         private fun getMsgPackageNameNoPackageVisibilityFilters(
478             method: UMethod,
479             argumentIndex: Int
480         ): String = "Api: ${method.name} contains a package name parameter: " +
481                 "${method.uastParameters[argumentIndex].name} does not apply " +
482                 "package visibility filtering rules."
483 
484         private val EXPLANATION = """
485             APIs working in the system_server and taking the package name as a parameter may have
486             chance to reveal package existence status on the device, and break the package
487             visibility that we introduced in Android 11.
488             (https://developer.android.com/about/versions/11/privacy/package-visibility)
489 
490             Take an example of the API `boolean setFoo(String packageName)`, a malicious app may
491             have chance to get package existence state on the device from the result of the API,
492             if there is no package visibility filtering rule or uid identify checks applying to
493             the parameter of the package name.
494 
495             To resolve it, you could apply package visibility filtering rules to the package name
496             via PackageManagerInternal.filterAppAccess API, before starting to use the package name.
497             If the parameter is a calling package name, use the PackageManager API such as
498             PackageManager.getPackagesForUid to verify the calling identify.
499             """
500 
501         val ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS = Issue.create(
502             id = "ApiMightLeakAppVisibility",
503             briefDescription = "Api takes package name parameter doesn't apply " +
504                     "package visibility filters",
505             explanation = EXPLANATION,
506             category = Category.SECURITY,
507             priority = 1,
508             severity = Severity.WARNING,
509             implementation = Implementation(
510                 PackageVisibilityDetector::class.java,
511                 Scope.JAVA_FILE_SCOPE
512             )
513         )
514     }
515 }
516