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