diff options
| author | 2022-07-28 19:07:25 +0000 | |
|---|---|---|
| committer | 2022-08-16 10:50:24 +0000 | |
| commit | a4664b66a3ea8f50b570e7bbf7bb34f04fa02439 (patch) | |
| tree | ccb390aaa94dfbb8ad02128c172193a8a200ddf8 | |
| parent | 2a7eb38239d2cb33d5a05a9e45dd47c171507376 (diff) | |
Add ManualPermissionCheckDetector
This linter looks at methods that implement an AIDL interface.
If a given method contains a simple permission check, it suggests
moving that check to an @EnforcePermission annotation. The intent
is to keep as many permission checks as possible at a lower-level
to the service implementation, thus mitigating permission bypass
vulnerabilities.
Also rearranges some helpers/constants for reuse, and moves everything related to aidl to its own package.
Test: atest ManualPermissionCheckDetectorTest --host
Bug: 232058525
Change-Id: Ie6eaf061d74bd773742aa47f731e95e4b137f438
10 files changed, 687 insertions, 31 deletions
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt index d19f4cceeaaf..12f3a1659313 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt @@ -19,6 +19,8 @@ package com.google.android.lint import com.android.tools.lint.client.api.IssueRegistry import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.detector.api.CURRENT_API +import com.google.android.lint.aidl.EnforcePermissionDetector +import com.google.android.lint.aidl.ManualPermissionCheckDetector import com.google.android.lint.parcel.SaferParcelChecker import com.google.auto.service.AutoService @@ -36,6 +38,7 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() { CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED, EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION, + ManualPermissionCheckDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS ) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt new file mode 100644 index 000000000000..82eb8ed8f621 --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint + +import com.google.android.lint.model.Method + +const val CLASS_STUB = "Stub" +const val CLASS_CONTEXT = "android.content.Context" +const val CLASS_ACTIVITY_MANAGER_SERVICE = "com.android.server.am.ActivityManagerService" +const val CLASS_ACTIVITY_MANAGER_INTERNAL = "android.app.ActivityManagerInternal" + +// Enforce permission APIs +val ENFORCE_PERMISSION_METHODS = listOf( + Method(CLASS_CONTEXT, "checkPermission"), + Method(CLASS_CONTEXT, "checkCallingPermission"), + Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"), + Method(CLASS_CONTEXT, "enforcePermission"), + Method(CLASS_CONTEXT, "enforceCallingPermission"), + Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"), + Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"), + Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") +) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt index 192dba17dd5b..48540b1da565 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt @@ -30,13 +30,13 @@ import com.android.tools.lint.detector.api.interprocedural.CallGraphResult import com.android.tools.lint.detector.api.interprocedural.searchForPaths import com.intellij.psi.PsiAnonymousClass import com.intellij.psi.PsiMethod +import java.util.LinkedList import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.UParameter import org.jetbrains.uast.USimpleNameReferenceExpression import org.jetbrains.uast.visitor.AbstractUastVisitor -import java.util.LinkedList /** * A lint checker to detect potential package visibility issues for system's APIs. APIs working @@ -362,14 +362,18 @@ class PackageVisibilityDetector : Detector(), SourceCodeScanner { name: String, matchArgument: Boolean = true, checkCaller: Boolean = false - ): this(clazz, name) { + ) : this(clazz, name) { this.matchArgument = matchArgument this.checkCaller = checkCaller } constructor( method: PsiMethod - ): this(method.containingClass?.qualifiedName ?: "", method.name) + ) : this(method.containingClass?.qualifiedName ?: "", method.name) + + constructor( + method: com.google.android.lint.model.Method + ) : this(method.clazz, method.name) } /** @@ -380,7 +384,7 @@ class PackageVisibilityDetector : Detector(), SourceCodeScanner { val typeName: String, val parameterName: String ) { - constructor(uParameter: UParameter): this( + constructor(uParameter: UParameter) : this( uParameter.type.canonicalText, uParameter.name.lowercase() ) @@ -405,19 +409,13 @@ class PackageVisibilityDetector : Detector(), SourceCodeScanner { // A valid call path list needs to contain a start node and a sink node private const val VALID_CALL_PATH_NODES_SIZE = 2 - private const val CLASS_STUB = "Stub" private const val CLASS_STRING = "java.lang.String" private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager" private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager" private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager" - private const val CLASS_CONTEXT = "android.content.Context" private const val CLASS_BINDER = "android.os.Binder" private const val CLASS_PACKAGE_MANAGER_INTERNAL = "android.content.pm.PackageManagerInternal" - private const val CLASS_ACTIVITY_MANAGER_SERVICE = - "com.android.server.am.ActivityManagerService" - private const val CLASS_ACTIVITY_MANAGER_INTERNAL = - "android.app.ActivityManagerInternal" // Patterns of package name parameter private val PACKAGE_NAME_PATTERNS = setOf( @@ -455,16 +453,9 @@ class PackageVisibilityDetector : Detector(), SourceCodeScanner { ) // Enforce permission APIs - private val ENFORCE_PERMISSION_METHODS = listOf( - Method(CLASS_CONTEXT, "checkPermission"), - Method(CLASS_CONTEXT, "checkCallingPermission"), - Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"), - Method(CLASS_CONTEXT, "enforcePermission"), - Method(CLASS_CONTEXT, "enforceCallingPermission"), - Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"), - Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"), - Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") - ) + private val ENFORCE_PERMISSION_METHODS = + com.google.android.lint.ENFORCE_PERMISSION_METHODS + .map(PackageVisibilityDetector::Method) private val BYPASS_STUBS = listOf( "android.content.pm.IPackageDataObserver.Stub", diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/Constants.kt new file mode 100644 index 000000000000..8ee3763e5079 --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/Constants.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint.aidl + +const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission" +const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission" +const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced" + +val AIDL_PERMISSION_ANNOTATIONS = listOf( + ANNOTATION_ENFORCE_PERMISSION, + ANNOTATION_REQUIRES_NO_PERMISSION, + ANNOTATION_PERMISSION_MANUALLY_ENFORCED +) + +const val IINTERFACE_INTERFACE = "android.os.IInterface" + +/** + * If a non java (e.g. c++) backend is enabled, the @EnforcePermission + * annotation cannot be used. At time of writing, the mechanism + * is not implemented for non java backends. + * TODO: b/242564874 (have lint know which interfaces have the c++ backend enabled) + * rather than hard coding this list? + */ +val EXCLUDED_CPP_INTERFACES = listOf( + "AdbTransportType", + "FingerprintAndPairDevice", + "IAdbCallback", + "IAdbManager", + "PairDevice", + "IStatsBootstrapAtomService", + "StatsBootstrapAtom", + "StatsBootstrapAtomValue", + "FixedSizeArrayExample", + "PlaybackTrackMetadata", + "RecordTrackMetadata", + "SinkMetadata", + "SourceMetadata", + "IUpdateEngineStable", + "IUpdateEngineStableCallback", + "AudioCapabilities", + "ConfidenceLevel", + "ModelParameter", + "ModelParameterRange", + "Phrase", + "PhraseRecognitionEvent", + "PhraseRecognitionExtra", + "PhraseSoundModel", + "Properties", + "RecognitionConfig", + "RecognitionEvent", + "RecognitionMode", + "RecognitionStatus", + "SoundModel", + "SoundModelType", + "Status", + "IThermalService", + "IPowerManager", + "ITunerResourceManager" +) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt index 9f216189ad62..a415217105a8 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.google.android.lint +package com.google.android.lint.aidl import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.AnnotationInfo import com.android.tools.lint.detector.api.AnnotationOrigin import com.android.tools.lint.detector.api.AnnotationUsageInfo import com.android.tools.lint.detector.api.AnnotationUsageType -import com.android.tools.lint.detector.api.ConstantEvaluator import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.ConstantEvaluator import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue @@ -34,8 +34,8 @@ import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import com.intellij.psi.PsiMethod import org.jetbrains.uast.UAnnotation -import org.jetbrains.uast.UElement import org.jetbrains.uast.UClass +import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod /** @@ -54,12 +54,11 @@ import org.jetbrains.uast.UMethod */ class EnforcePermissionDetector : Detector(), SourceCodeScanner { - val ENFORCE_PERMISSION = "android.annotation.EnforcePermission" val BINDER_CLASS = "android.os.Binder" val JAVA_OBJECT = "java.lang.Object" override fun applicableAnnotations(): List<String> { - return listOf(ENFORCE_PERMISSION) + return listOf(ANNOTATION_ENFORCE_PERMISSION) } override fun getApplicableUastTypes(): List<Class<out UElement>> { @@ -99,8 +98,8 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { overriddenMethod: PsiMethod, checkEquivalence: Boolean = true ) { - val overridingAnnotation = overridingMethod.getAnnotation(ENFORCE_PERMISSION) - val overriddenAnnotation = overriddenMethod.getAnnotation(ENFORCE_PERMISSION) + val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) val location = context.getLocation(element) val overridingClass = overridingMethod.parent as PsiClass val overriddenClass = overriddenMethod.parent as PsiClass @@ -133,8 +132,8 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { extendedClass: PsiClass, checkEquivalence: Boolean = true ) { - val newAnnotation = newClass.getAnnotation(ENFORCE_PERMISSION) - val extendedAnnotation = extendedClass.getAnnotation(ENFORCE_PERMISSION) + val newAnnotation = newClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val extendedAnnotation = extendedClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) val location = context.getLocation(element) val newClassName = newClass.qualifiedName @@ -180,7 +179,7 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitAnnotation(node: UAnnotation) { - if (node.qualifiedName != ENFORCE_PERMISSION) { + if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) { return } val method = node.uastParent as? UMethod diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt new file mode 100644 index 000000000000..510611161ea8 --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Location +import com.intellij.psi.PsiVariable +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.USimpleNameReferenceExpression +import org.jetbrains.uast.asRecursiveLogString + +/** + * Helper ADT class that facilitates the creation of lint auto fixes + * + * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks + * that should be migrated to @EnforcePermission(allOf={...}) + * + * TODO: handle anyOf style annotations + */ +sealed class EnforcePermissionFix { + abstract fun locations(): List<Location> + abstract fun javaAnnotationParameter(): String + + fun javaAnnotation(): String = "@$ANNOTATION_ENFORCE_PERMISSION(${javaAnnotationParameter()})" + + companion object { + fun fromCallExpression(callExpression: UCallExpression, context: JavaContext): SingleFix = + SingleFix( + getPermissionCheckLocation(context, callExpression), + getPermissionCheckArgumentValue(callExpression) + ) + + fun maybeAddManifestPrefix(permissionName: String): String = + if (permissionName.contains(".")) permissionName + else "android.Manifest.permission.$permissionName" + + /** + * Given a permission check, get its proper location + * so that a lint fix can remove the entire expression + */ + private fun getPermissionCheckLocation( + context: JavaContext, + callExpression: UCallExpression + ): + Location { + val javaPsi = callExpression.javaPsi!! + return Location.create( + context.file, + javaPsi.containingFile?.text, + javaPsi.textRange.startOffset, + // unfortunately the element doesn't include the ending semicolon + javaPsi.textRange.endOffset + 1 + ) + } + + /** + * Given a permission check and an argument, + * pull out the permission value that is being used + */ + private fun getPermissionCheckArgumentValue( + callExpression: UCallExpression, + argumentPosition: Int = 0 + ): String { + + val identifier = when ( + val argument = callExpression.valueArguments.getOrNull(argumentPosition) + ) { + is UQualifiedReferenceExpression -> when (val selector = argument.selector) { + is USimpleNameReferenceExpression -> + ((selector.resolve() as PsiVariable).computeConstantValue() as String) + + else -> throw RuntimeException( + "Couldn't resolve argument: ${selector.asRecursiveLogString()}" + ) + } + + is USimpleNameReferenceExpression -> ( + (argument.resolve() as PsiVariable).computeConstantValue() as String) + + is ULiteralExpression -> argument.value as String + + else -> throw RuntimeException( + "Couldn't resolve argument: ${argument?.asRecursiveLogString()}" + ) + } + + return identifier.substringAfterLast(".") + } + } +} + +data class SingleFix(val location: Location, val permissionName: String) : EnforcePermissionFix() { + override fun locations(): List<Location> = listOf(this.location) + override fun javaAnnotationParameter(): String = maybeAddManifestPrefix(this.permissionName) +} +data class AllOfFix(val checks: List<SingleFix>) : EnforcePermissionFix() { + override fun locations(): List<Location> = this.checks.map { it.location } + override fun javaAnnotationParameter(): String = + "allOf={${ + this.checks.joinToString(", ") { maybeAddManifestPrefix(it.permissionName) } + }}" +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt new file mode 100644 index 000000000000..2cea39423f1d --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.google.android.lint.CLASS_STUB +import com.google.android.lint.ENFORCE_PERMISSION_METHODS +import com.intellij.psi.PsiAnonymousClass +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UQualifiedReferenceExpression + +/** + * Looks for methods implementing generated AIDL interface stubs + * that can have simple permission checks migrated to + * @EnforcePermission annotations + * + * TODO: b/242564870 (enable parse and autoFix of .aidl files) + */ +@Suppress("UnstableApiUsage") +class ManualPermissionCheckDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List<Class<out UElement?>> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + val interfaceName = getContainingAidlInterface(node) + .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return + val body = (node.uastBody as? UBlockExpression) ?: return + val fix = accumulateSimplePermissionCheckFixes(body) ?: return + + val javaRemoveFixes = fix.locations().map { + fix() + .replace() + .reformat(true) + .range(it) + .with("") + .autoFix() + .build() + } + + val javaAnnotateFix = fix() + .annotate(fix.javaAnnotation()) + .range(context.getLocation(node)) + .autoFix() + .build() + + val message = + "$interfaceName permission check can be converted to @EnforcePermission annotation" + + context.report( + ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, + fix.locations().last(), + message, + fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix) + ) + } + + /** + * Walk the expressions in the method, looking for simple permission checks. + * + * If a single permission check is found at the beginning of the method, + * this should be migrated to @EnforcePermission(value). + * + * If multiple consecutive permission checks are found, + * these should be migrated to @EnforcePermission(allOf={value1, value2, ...}) + * + * As soon as something other than a permission check is encountered, stop looking, + * as some other business logic is happening that prevents an automated fix. + */ + private fun accumulateSimplePermissionCheckFixes(methodBody: UBlockExpression): + EnforcePermissionFix? { + val singleFixes = mutableListOf<SingleFix>() + for (expression in methodBody.expressions) { + singleFixes.add(getPermissionCheckFix(expression) ?: break) + } + return when (singleFixes.size) { + 0 -> null + 1 -> singleFixes[0] + else -> AllOfFix(singleFixes) + } + } + + /** + * If an expression boils down to a permission check, return + * the helper for creating a lint auto fix to @EnforcePermission + */ + private fun getPermissionCheckFix(startingExpression: UElement?): + SingleFix? { + return when (startingExpression) { + is UQualifiedReferenceExpression -> getPermissionCheckFix( + startingExpression.selector + ) + + is UIfExpression -> getPermissionCheckFix(startingExpression.condition) + + is UCallExpression -> { + return if (isPermissionCheck(startingExpression)) + EnforcePermissionFix.fromCallExpression(startingExpression, context) + else null + } + + else -> null + } + } + } + + companion object { + + private val EXPLANATION = """ + Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission + annotation to declare the permissions to be enforced. The verification code is then + generated by the AIDL compiler, which also takes care of annotating the generated java + code. + + This reduces the risk of bugs around these permission checks (that often become vulnerabilities). + It also enables easier auditing and review. + + Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto) + """.trimIndent() + + @JvmField + val ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION = Issue.create( + id = "UseEnforcePermissionAnnotation", + briefDescription = "Manual permission check can be @EnforcePermission annotation", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + ManualPermissionCheckDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = false, // TODO: enable once b/241171714 is resolved + ) + + private fun isPermissionCheck(callExpression: UCallExpression): Boolean { + val method = callExpression.resolve() ?: return false + val className = method.containingClass?.qualifiedName + return ENFORCE_PERMISSION_METHODS.any { + it.clazz == className && it.name == method.name + } + } + + /** + * given a UMethod, determine if this method is + * an entrypoint to an interface generated by AIDL, + * returning the interface name if so + */ + fun getContainingAidlInterface(node: UMethod): String? { + if (!isInClassCalledStub(node)) return null + for (superMethod in node.findSuperMethods()) { + for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements + ?: continue) { + if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) { + return superMethod.containingClass?.name + } + } + } + return null + } + + private fun isInClassCalledStub(node: UMethod): Boolean { + (node.containingClass as? PsiAnonymousClass)?.let { + return it.baseClassReference.referenceName == CLASS_STUB + } + return node.containingClass?.extendsList?.referenceElements?.any { + it.referenceName == CLASS_STUB + } ?: false + } + } +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/model/Method.kt b/tools/lint/checks/src/main/java/com/google/android/lint/model/Method.kt new file mode 100644 index 000000000000..3939b6109eaa --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/model/Method.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint.model + +/** + * Data class to represent a Method + */ +data class Method(val clazz: String, val name: String) { + override fun toString(): String { + return "$clazz#$name" + } +} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt index 2cfc3fbcefcb..3c1d1e8e8a59 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt +++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.lint +package com.google.android.lint.aidl import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFile diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt new file mode 100644 index 000000000000..1a1c6bc77785 --- /dev/null +++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class ManualPermissionCheckDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = ManualPermissionCheckDetector() + override fun getIssues(): List<Issue> = listOf( + ManualPermissionCheckDetector + .ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] + mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS) + @@ -7 +8 + - mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); + """ + ) + } + + fun testAnonClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.Manifest.permission.READ_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 8: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.Manifest.permission.READ_CONTACTS", "foo"); + """ + ) + } + + fun testAllOf() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.Manifest.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission( + "android.Manifest.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.Manifest.permission.READ_CONTACTS", "foo"); + - mContext.enforceCallingOrSelfPermission( + - "android.Manifest.permission.WRITE_CONTACTS", "foo"); + """ + ) + } + + fun testPrecedingExpressions() { + lint().files( + java( + """ + import android.os.Binder; + import android.test.ITest; + public class Foo extends ITest.Stub { + private mContext Context; + @Override + public void test() throws android.os.RemoteException { + long uid = Binder.getCallingUid(); + mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + companion object { + private val aidlStub: TestFile = java( + """ + package android.test; + public interface ITest extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements android.test.ITest {} + public void test() throws android.os.RemoteException; + } + """ + ).indented() + + private val contextStub: TestFile = java( + """ + package android.content; + public class Context { + public void enforceCallingOrSelfPermission(String permission, String message) {} + } + """ + ).indented() + + private val binderStub: TestFile = java( + """ + package android.os; + public class Binder { + public static int getCallingUid() {} + } + """ + ).indented() + + val stubs = arrayOf(aidlStub, contextStub, binderStub) + } +} |