blob: 4ec6c40b87547137d22feb5793461d947016803e [file] [log] [blame]
/*
* Copyright (C) 2017 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.android.tools.lint.checks
import com.android.SdkConstants.ANDROID_URI
import com.android.SdkConstants.ATTR_NAME
import com.android.SdkConstants.ATTR_REQUIRED
import com.android.SdkConstants.CLASS_INTENT
import com.android.SdkConstants.TAG_APPLICATION
import com.android.SdkConstants.TAG_PERMISSION
import com.android.SdkConstants.TAG_USES_LIBRARY
import com.android.SdkConstants.TAG_USES_PERMISSION
import com.android.SdkConstants.TAG_USES_PERMISSION_SDK_23
import com.android.SdkConstants.TAG_USES_PERMISSION_SDK_M
import com.android.SdkConstants.VALUE_FALSE
import com.android.tools.lint.checks.AnnotationDetector.PERMISSION_ANNOTATION
import com.android.tools.lint.checks.AnnotationDetector.PERMISSION_ANNOTATION_READ
import com.android.tools.lint.checks.AnnotationDetector.PERMISSION_ANNOTATION_WRITE
import com.android.tools.lint.checks.AnnotationDetector.SECURITY_EXCEPTION
import com.android.tools.lint.checks.PermissionFinder.Operation.ACTION
import com.android.tools.lint.checks.PermissionFinder.Operation.READ
import com.android.tools.lint.checks.PermissionFinder.Operation.WRITE
import com.android.tools.lint.checks.PermissionRequirement.ATTR_PROTECTION_LEVEL
import com.android.tools.lint.checks.PermissionRequirement.VALUE_DANGEROUS
import com.android.tools.lint.detector.api.AnnotationUsageType
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ConstantEvaluator
import com.android.tools.lint.detector.api.Context
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.android.utils.XmlUtils
import com.google.common.collect.Sets
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiType
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UTryExpression
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.util.isMethodCall
import org.jetbrains.uast.visitor.AbstractUastVisitor
import java.util.Arrays
class PermissionDetector : AbstractAnnotationDetector(), SourceCodeScanner {
override fun applicableAnnotations(): List<String> = listOf(
PERMISSION_ANNOTATION.oldName(),
PERMISSION_ANNOTATION.newName(),
PERMISSION_ANNOTATION_READ.oldName(),
PERMISSION_ANNOTATION_READ.newName(),
PERMISSION_ANNOTATION_WRITE.oldName(),
PERMISSION_ANNOTATION_WRITE.newName()
)
override fun visitAnnotationUsage(
context: JavaContext,
usage: UElement,
type: AnnotationUsageType,
annotation: UAnnotation,
qualifiedName: String,
method: PsiMethod?,
referenced: PsiElement?,
annotations: List<UAnnotation>,
allMemberAnnotations: List<UAnnotation>,
allClassAnnotations: List<UAnnotation>,
allPackageAnnotations: List<UAnnotation>
) {
if (annotations === allMemberAnnotations) {
// Permission annotation specified on method:
val requirement = PermissionRequirement.create(annotation)
checkPermission(context, usage, method, null, requirement)
} else {
// PERMISSION_ANNOTATION, PERMISSION_ANNOTATION_READ, PERMISSION_ANNOTATION_WRITE
// When specified on a parameter, that indicates that we're dealing with
// a permission requirement on this *method* which depends on the value
// supplied by this parameter
if (usage is UExpression && method != null) {
checkParameterPermission(context, qualifiedName, method, usage)
}
}
}
private fun checkParameterPermission(
context: JavaContext,
signature: String,
method: PsiMethod,
argument: UExpression
) {
var operation: PermissionFinder.Operation? = null
if (PERMISSION_ANNOTATION_READ.isEquals(signature)) {
operation = READ
} else if (PERMISSION_ANNOTATION_WRITE.isEquals(signature)) {
operation = WRITE
} else {
val type = argument.getExpressionType()
if (type != null && CLASS_INTENT == type.canonicalText) {
operation = ACTION
}
}
if (operation == null) {
return
}
val result = PermissionFinder.findRequiredPermissions(context, operation, argument)
if (result != null) {
checkPermission(context, argument, method, result, result.requirement)
}
}
private fun checkPermission(
context: JavaContext,
node: UElement,
method: PsiMethod?,
result: PermissionFinder.Result?,
requirement: PermissionRequirement
) {
if (requirement.isConditional) {
return
}
var permissions = getPermissions(context)
if (!requirement.isSatisfied(permissions)) {
// See if it looks like we're holding the permission implicitly by @RequirePermission
// annotations in the surrounding context
permissions = addLocalPermissions(permissions, node)
if (!requirement.isSatisfied(permissions)) {
val operation: PermissionFinder.Operation
val name: String
if (result != null) {
name = result.name
operation = result.operation
} else {
assert(method != null)
val containingClass = method!!.containingClass
name = if (containingClass != null) {
containingClass.name + "." + method.name
} else {
method.name
}
if (name == "Builder.setPersisted" && node is UCallExpression &&
node.valueArguments.size == 1 &&
ConstantEvaluator.evaluate(context, node.valueArguments[0]) == false
) {
// Special case the JobInfo.Builder permission requirement: it only
// applies if the argument is true. If we're not sure, default to
// flagging it.
return
}
operation = PermissionFinder.Operation.CALL
}
val message = String.format(
"Missing permissions required %1\$s %2\$s: %3\$s", operation.prefix(),
name, requirement.describeMissingPermissions(permissions)
).replace(
"carrier privileges",
"carrier privileges (see TelephonyManager#hasCarrierPrivileges)"
)
// Report locations on the call, not just the flown parameter
var location = context.getLocation(node)
val expressionNode =
node.getParentOfType<UCallExpression>(UCallExpression::class.java, true)
if (expressionNode != null) {
val callIdentifier = expressionNode.methodIdentifier
if (callIdentifier != null && callIdentifier != node) {
location = context.getRangeLocation(callIdentifier, 0, node, 0)
}
}
val missingPermissions = requirement.getMissingPermissions(permissions)
if (method?.name == "getAllCellInfo" &&
missingPermissions.size == 1 &&
missingPermissions.first() == "android.permission.ACCESS_COARSE_LOCATION" &&
permissions.hasPermission("android.permission.ACCESS_FINE_LOCATION")
) {
// Special case for issue 63962416:
// TelephonyManager.getAllCellInfo is incorrectly annotated as requiring
// ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION; instead it's annotated
// as just requiring ACCESS_COARSE_LOCATION but clearly holding
// ACCESS_FINE_LOCATION is acceptable too.
return
}
report(
context, MISSING_PERMISSION, node, location, message,
// Pass data to IDE quickfix: names to add, and max applicable API version
fix().data(
missingPermissions,
requirement.lastApplicableApi
)
)
}
} else if (requirement.isRevocable(permissions) &&
context.mainProject.targetSdkVersion.featureLevel >= 23 &&
requirement.lastApplicableApi >= 23 &&
!isAndroidThingsProject(context)
) {
var handlesMissingPermission = handlesSecurityException(node)
// If not, check to see if the code is deliberately checking to see if the
// given permission is available.
if (!handlesMissingPermission) {
// See if the requirement is passed on via surrounding requires permissions
val localRequirements = addLocalPermissions(
PermissionHolder.SetPermissionLookup(
mutableSetOf(), mutableSetOf(),
permissions.minSdkVersion, permissions.targetSdkVersion
), node
)
if (requirement.isSatisfied(localRequirements)) {
return
}
val methodNode = node.getParentOfType<UMethod>(UMethod::class.java, true)
if (methodNode != null) {
val visitor = CheckPermissionVisitor(node)
methodNode.accept(visitor)
handlesMissingPermission = visitor.checksPermission()
}
}
if (!handlesMissingPermission) {
val message =
"Call requires permission which may be rejected by user: code should explicitly " +
"check to see if permission is available (with `checkPermission`) or explicitly " +
"handle a potential `SecurityException`"
val location = context.getLocation(node)
report(
context, MISSING_PERMISSION, node, location, message,
// Pass data to IDE quickfix: revocable names, and permission requirement
fix().data(
requirement.getRevocablePermissions(permissions),
requirement
)
)
}
}
}
private fun handlesSecurityException(node: UElement): Boolean {
// Ensure that the caller is handling a security exception
// First check to see if we're inside a try/catch which catches a SecurityException
// (or some wider exception than that). Check for nested try/catches too.
var parent = node
while (true) {
val tryCatch = parent.getParentOfType<UTryExpression>(UTryExpression::class.java, true)
if (tryCatch == null) {
break
} else {
for (catchClause in tryCatch.catchClauses) {
if (containsSecurityException(catchClause.types)) {
return true
}
}
parent = tryCatch
}
}
// If not, check to see if the method itself declares that it throws a
// SecurityException or something wider.
val declaration = parent.getParentOfType<UMethod>(UMethod::class.java, false)
if (declaration != null) {
val thrownTypes = declaration.throwsList.referencedTypes
if (containsSecurityException(Arrays.asList<PsiClassType>(*thrownTypes))) {
return true
}
}
return false
}
private fun addLocalPermissions(
permissions: PermissionHolder,
node: UElement
): PermissionHolder {
var merged = permissions
// Accumulate @RequirePermissions available in the local context
val method = node.getParentOfType<UMethod>(UMethod::class.java, true) ?: return merged
var annotation =
method.findAnnotation(PERMISSION_ANNOTATION.oldName()) ?: method.findAnnotation(
PERMISSION_ANNOTATION.newName()
)
merged = mergeAnnotationPermissions(merged, annotation)
val containingClass = method.getContainingUClass()
if (containingClass != null) {
annotation = containingClass.findAnnotation(PERMISSION_ANNOTATION.oldName())
?: containingClass.findAnnotation(PERMISSION_ANNOTATION.newName())
merged = mergeAnnotationPermissions(merged, annotation)
}
return merged
}
private fun mergeAnnotationPermissions(
permissions: PermissionHolder,
annotation: UAnnotation?
): PermissionHolder {
var merged = permissions
if (annotation != null) {
val requirement = PermissionRequirement.create(annotation)
merged = PermissionHolder.SetPermissionLookup.join(merged, requirement)
}
return merged
}
/**
* Visitor which looks through a method, up to a given call (the one requiring a
* permission) and checks whether it's preceded by a call to checkPermission or
* checkCallingPermission or enforcePermission etc.
*
*
* Currently it only looks for the presence of this check; it does not perform
* flow analysis to determine whether the check actually affects program flow
* up to the permission call, or whether the check permission is checking for
* permissions sufficient to satisfy the permission requirement of the target call,
* or whether the check return value (== PERMISSION_GRANTED vs != PERMISSION_GRANTED)
* is handled correctly, etc.
*/
private class CheckPermissionVisitor(private val mTarget: UElement) : AbstractUastVisitor() {
private var mChecksPermission: Boolean = false
private var mDone: Boolean = false
override fun visitElement(node: UElement): Boolean {
return mDone || super.visitElement(node)
}
override fun visitCallExpression(node: UCallExpression): Boolean {
if (node.isMethodCall()) {
visitMethodCallExpression(node)
}
return super.visitCallExpression(node)
}
private fun visitMethodCallExpression(node: UCallExpression) {
if (node === mTarget) {
mDone = true
}
val name = node.methodName
if (name != null &&
(name.startsWith("check") || name.startsWith("enforce")) &&
name.endsWith("Permission")
) {
mChecksPermission = true
mDone = true
}
}
fun checksPermission(): Boolean {
return mChecksPermission
}
}
private fun containsSecurityException(
types: List<PsiType>
): Boolean {
for (type in types) {
if (type is PsiClassType) {
val cls = type.resolve()
// In earlier versions we checked not just for java.lang.SecurityException but
// any super type as well, however that probably hides warnings in cases where
// users don't want that; see http://b.android.com/182165
// return context.getEvaluator().extendsClass(cls, "java.lang.SecurityException", false);
if (cls != null && SECURITY_EXCEPTION == cls.qualifiedName) {
return true
}
}
}
return false
}
private var mPermissions: PermissionHolder? = null
private fun getPermissions(
context: JavaContext
): PermissionHolder {
if (mPermissions == null) {
val permissions = Sets.newHashSetWithExpectedSize<String>(30)
val revocable = Sets.newHashSetWithExpectedSize<String>(4)
val mainProject = context.mainProject
val mergedManifest = mainProject.mergedManifest
if (mergedManifest != null) {
for (element in XmlUtils.getSubTags(mergedManifest.documentElement)) {
val nodeName = element.nodeName
if (TAG_USES_PERMISSION == nodeName ||
TAG_USES_PERMISSION_SDK_23 == nodeName ||
TAG_USES_PERMISSION_SDK_M == nodeName
) {
val name = element.getAttributeNS(ANDROID_URI, ATTR_NAME)
if (!name.isEmpty()) {
permissions.add(name)
}
} else if (nodeName == TAG_PERMISSION) {
val protectionLevel = element.getAttributeNS(
ANDROID_URI,
ATTR_PROTECTION_LEVEL
)
if (VALUE_DANGEROUS == protectionLevel) {
val name = element.getAttributeNS(ANDROID_URI, ATTR_NAME)
if (!name.isEmpty()) {
revocable.add(name)
}
}
}
}
}
val minSdkVersion = mainProject.minSdkVersion
val targetSdkVersion = mainProject.targetSdkVersion
mPermissions = PermissionHolder.SetPermissionLookup(
permissions,
revocable,
minSdkVersion,
targetSdkVersion
)
}
return mPermissions!!
}
private var mIsAndroidThingsProject: Boolean? = null
private fun isAndroidThingsProject(context: Context): Boolean {
if (mIsAndroidThingsProject == null) {
val project = context.mainProject
val mergedManifest = project.mergedManifest ?: return false
val manifest = mergedManifest.documentElement ?: return false
val application = XmlUtils.getFirstSubTagByName(
manifest,
TAG_APPLICATION
) ?: return false
var usesLibrary = XmlUtils.getFirstSubTagByName(application, TAG_USES_LIBRARY)
while (usesLibrary != null && mIsAndroidThingsProject == null) {
val name = usesLibrary.getAttributeNS(ANDROID_URI, ATTR_NAME)
var isThingsLibraryRequired = true
val required = usesLibrary.getAttributeNS(ANDROID_URI, ATTR_REQUIRED)
if (VALUE_FALSE == required) {
isThingsLibraryRequired = false
}
if (THINGS_LIBRARY == name && isThingsLibraryRequired) {
mIsAndroidThingsProject = true
}
usesLibrary = XmlUtils.getNextTagByName(usesLibrary, TAG_USES_LIBRARY)
}
if (mIsAndroidThingsProject == null) {
mIsAndroidThingsProject = false
}
}
return mIsAndroidThingsProject!!
}
companion object {
private val IMPLEMENTATION = Implementation(
PermissionDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
private const val THINGS_LIBRARY = "com.google.android.things"
/** Method result should be used */
@JvmField
val MISSING_PERMISSION = Issue.create(
id = "MissingPermission",
briefDescription = "Missing Permissions",
explanation = """
This check scans through your code and libraries and looks at the APIs being \
used, and checks this against the set of permissions required to access \
those APIs. If the code using those APIs is called at runtime, then the \
program will crash.
Furthermore, for permissions that are revocable (with `targetSdkVersion` 23), \
client code must also be prepared to handle the calls throwing an exception \
if the user rejects the request for permission at runtime.""",
category = Category.CORRECTNESS,
priority = 9,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION
)
}
}