summaryrefslogtreecommitdiff
path: root/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnsafeIntentLaunchDetector.kt
diff options
context:
space:
mode:
Diffstat (limited to 'lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnsafeIntentLaunchDetector.kt')
-rw-r--r--lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnsafeIntentLaunchDetector.kt275
1 files changed, 220 insertions, 55 deletions
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnsafeIntentLaunchDetector.kt b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnsafeIntentLaunchDetector.kt
index e43dce6aae..5bd51d4405 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnsafeIntentLaunchDetector.kt
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnsafeIntentLaunchDetector.kt
@@ -20,6 +20,9 @@ import com.android.SdkConstants.ANDROID_URI
import com.android.SdkConstants.ATTR_EXPORTED
import com.android.SdkConstants.ATTR_NAME
import com.android.SdkConstants.ATTR_PERMISSION
+import com.android.SdkConstants.TAG_ACTIVITY
+import com.android.SdkConstants.TAG_RECEIVER
+import com.android.SdkConstants.TAG_SERVICE
import com.android.tools.lint.checks.BroadcastReceiverUtils.BROADCAST_RECEIVER_METHOD_NAMES
import com.android.tools.lint.client.api.JavaEvaluator
import com.android.tools.lint.client.api.TYPE_INT
@@ -38,25 +41,37 @@ 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.tools.lint.detector.api.UastLintUtils
+import com.android.tools.lint.detector.api.UastLintUtils.Companion.findConstruction
import com.android.tools.lint.detector.api.UastLintUtils.Companion.findLastAssignment
import com.android.tools.lint.detector.api.XmlContext
import com.android.tools.lint.detector.api.XmlScanner
import com.android.tools.lint.detector.api.findSelector
+import com.android.tools.lint.detector.api.isReturningContext
import com.android.tools.lint.detector.api.isReturningLambdaResult
import com.android.tools.lint.detector.api.isScopingThis
+import com.android.utils.iterator
+import com.android.utils.subtag
import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiSwitchLabelStatementBase
import com.intellij.psi.PsiVariable
import java.util.EnumSet
+import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.UReturnExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.USwitchClauseExpression
+import org.jetbrains.uast.USwitchExpression
import org.jetbrains.uast.UThisExpression
+import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.getQualifiedName
import org.jetbrains.uast.isNullLiteral
@@ -88,24 +103,49 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
override fun getApplicableElements() =
listOf(
- SdkConstants.TAG_ACTIVITY,
- SdkConstants.TAG_SERVICE,
- SdkConstants.TAG_RECEIVER,
+ TAG_ACTIVITY,
+ TAG_SERVICE,
+ TAG_RECEIVER,
)
override fun visitElement(context: XmlContext, element: Element) {
- val exportedAttr = element.getAttributeNS(ANDROID_URI, ATTR_EXPORTED)
+ storeUnprotectedComponents(context, getProtectedComponent(context, element) ?: return)
+ }
+
+ private fun isComponentExported(
+ context: Context,
+ root: Element,
+ incidentComponent: String?
+ ): Boolean {
+ val application = root.subtag(SdkConstants.TAG_APPLICATION) ?: return false
+ for (component in application) {
+ when (component.tagName) {
+ TAG_ACTIVITY,
+ TAG_RECEIVER,
+ TAG_SERVICE -> {
+ if (incidentComponent == getProtectedComponent(context, component)) return true
+ }
+ }
+ }
+ return false
+ }
+
+ // Returns the fully qualified component name if the component is protected; otherwise, null
+ // The element passed in is guaranteed to be one of the activity, receiver or service tag.
+ private fun getProtectedComponent(context: Context, component: Element): String? {
+ val exportedAttr = component.getAttributeNS(ANDROID_URI, ATTR_EXPORTED)
if (
"true" == exportedAttr ||
- exportedAttr.isEmpty() && element.getElementsByTagName("intent-filter").length > 0
+ exportedAttr.isEmpty() && component.getElementsByTagName("intent-filter").length > 0
) {
- val permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION)
+ val permission = component.getAttributeNS(ANDROID_URI, ATTR_PERMISSION)
if (!isProbablyProtectedBySignaturePermission(permission)) {
- var componentName = element.getAttributeNS(ANDROID_URI, ATTR_NAME)
+ var componentName = component.getAttributeNS(ANDROID_URI, ATTR_NAME)
if (componentName.startsWith(".")) componentName = context.project.`package` + componentName
- storeUnprotectedComponents(context, componentName)
+ return componentName
}
}
+ return null
}
// Any permission that is not declared by the system as normal permission is considered as a
@@ -144,10 +184,13 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
initial = setOf(intentParam ?: return),
context = context,
location = context.getLocation(intentParam.sourcePsi),
+ checkProtectedBroadcast =
+ UNSAFE_INTENT_AS_PARAMETER_METHODS[BROADCAST_RECEIVER_CLASS]?.contains(methodName) ==
+ true
)
method.accept(visitor)
if (visitor.launched) {
- storeIncidentsToPartialResults(context, visitor)
+ reportIncident(context, visitor)
}
}
}
@@ -175,18 +218,11 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
containingMethod?.accept(visitor)
if (visitor.launched) {
if (visitor.unprotectedReceiver) {
- // The registered-at-runtime receiver case, report the issue immediately.
- val message =
- """
- This intent could be coming from an untrusted source. It is later launched by \
- an unprotected component. You could either make the component \
- protected; or sanitize this intent using \
- androidx.core.content.IntentSanitizer.
- """
- .trimIndent()
- context.report(Incident(ISSUE, visitor.location, message))
+ // The anonymous component registered-at-runtime receiver case, report the issue
+ // immediately.
+ reportIssue(context, null, visitor.location)
} else {
- storeIncidentsToPartialResults(context, visitor)
+ reportIncident(context, visitor)
}
}
}
@@ -237,18 +273,19 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
val receiverArg = UastLintUtils.findArgument(call, method, BROADCAST_RECEIVER_CLASS) ?: return
if (receiverArg.isNullLiteral()) return
- if (!isRuntimeReceiverProtected(call, method)) {
- val receiverVar = receiverArg.tryResolve() as? PsiVariable ?: return
- val receiverAssignment =
- findLastAssignment(receiverVar, call)?.skipParenthesizedExprDown() ?: return
- val receiverConstructor = receiverAssignment.findSelector() as? UCallExpression
+ if (!isRuntimeReceiverProtected(call, method, context.evaluator)) {
+ val receiverConstructor = findConstruction(BROADCAST_RECEIVER_CLASS, receiverArg, call, true)
val unprotectedReceiverClassName =
receiverConstructor?.classReference.getQualifiedName() ?: return
storeUnprotectedComponents(context, unprotectedReceiverClassName)
}
}
- fun isRuntimeReceiverProtected(call: UCallExpression, method: PsiMethod): Boolean {
+ fun isRuntimeReceiverProtected(
+ call: UCallExpression,
+ method: PsiMethod,
+ javaEvaluator: JavaEvaluator
+ ): Boolean {
// The parameter positions vary across the various registerReceiver*() methods, so rather
// than hardcode them we simply look them up based on the parameter name and type.
@@ -267,7 +304,7 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
BroadcastReceiverUtils.checkIsProtectedReceiverAndReturnUnprotectedActions(
filterArg,
call,
- evaluator
+ javaEvaluator
)
return isProtected
@@ -281,18 +318,31 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
unprotectedComponents.put(unprotectedComponentName, true)
}
- private fun storeIncidentsToPartialResults(context: Context, visitor: IntentLaunchChecker) {
- val lintMap = context.getPartialResults(ISSUE).map()
- val incidents = lintMap.getMap(KEY_INCIDENTS) ?: map().also { lintMap.put(KEY_INCIDENTS, it) }
- // key is not important. so the size of the map is used to make it unique.
- incidents.put(
- incidents.size.toString(),
- map().apply {
- put(KEY_LOCATION, visitor.location)
- put(KEY_SECONDARY_LOCATION, visitor.location.secondary ?: return)
- put(KEY_INCIDENT_CLASS, visitor.incidentClass ?: return)
+ private fun reportIncident(context: Context, visitor: IntentLaunchChecker) {
+ if (context.isGlobalAnalysis()) {
+ val incidentComponent = visitor.incidentClass
+ if (
+ isComponentExported(
+ context,
+ context.mainProject.mergedManifest?.documentElement ?: return,
+ incidentComponent
+ )
+ ) {
+ reportIssue(context, incidentComponent, visitor.location)
}
- )
+ } else {
+ val lintMap = context.getPartialResults(ISSUE).map()
+ val incidents = lintMap.getMap(KEY_INCIDENTS) ?: map().also { lintMap.put(KEY_INCIDENTS, it) }
+ // key is not important. so the size of the map is used to make it unique.
+ incidents.put(
+ incidents.size.toString(),
+ map().apply {
+ put(KEY_LOCATION, visitor.location)
+ put(KEY_SECONDARY_LOCATION, visitor.location.secondary ?: return)
+ put(KEY_INCIDENT_CLASS, visitor.incidentClass ?: return)
+ }
+ )
+ }
}
override fun afterCheckRootProject(context: Context) {
@@ -310,19 +360,23 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
if (unprotectedComponents.containsKey(incidentComponent)) {
val location = incidentMap.getLocation(KEY_LOCATION) ?: continue
location.secondary = incidentMap.getLocation(KEY_SECONDARY_LOCATION)
- val message =
- """
- This intent could be coming from an untrusted source. It is later launched by \
- an unprotected component $incidentComponent. You could either make the component \
- $incidentComponent protected; or sanitize this intent using \
- androidx.core.content.IntentSanitizer.
- """
- .trimIndent()
- context.report(Incident(ISSUE, location, message))
+ reportIssue(context, incidentComponent, location)
}
}
}
+ private fun reportIssue(context: Context, incidentComponent: String?, location: Location) {
+ val component = if (incidentComponent.isNullOrBlank()) "" else " $incidentComponent"
+ val message =
+ """
+ This intent could be coming from an untrusted source. It is later launched by \
+ an unprotected component$component. You could either make the component$component \
+ protected; or sanitize this intent using androidx.core.content.IntentSanitizer.
+ """
+ .trimIndent()
+ context.report(Incident(ISSUE, location, message))
+ }
+
private inner class IntentLaunchChecker(
initial: Collection<UElement>,
var context: JavaContext,
@@ -331,7 +385,8 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
var launched: Boolean = false,
var returned: Boolean = false,
var unprotectedReceiver: Boolean = false,
- var resolveCallDepth: Int = 0
+ var resolveCallDepth: Int = 0,
+ var checkProtectedBroadcast: Boolean = false
) : DataFlowAnalyzer(initial) {
override fun returnsSelf(call: UCallExpression): Boolean {
@@ -374,9 +429,11 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
}
}
if (isIntentLaunchedBySystem(context.evaluator, call)) {
- launched = true
- location.secondary = context.getLocation(call)
- location.secondary?.message = "The unsafe intent is launched here."
+ if (!checkProtectedBroadcast || !inProtectedBroadcastBranch(context, call, reference)) {
+ launched = true
+ location.secondary = context.getLocation(call)
+ location.secondary?.message = "The unsafe intent is launched here."
+ }
} else {
if (resolveCallDepth > MAX_CALL_DEPTH) return
// escaped to another method call. check the method recursively.
@@ -393,7 +450,7 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
)
containingMethod.accept(visitor)
if (visitor.launched) {
- storeIncidentsToPartialResults(context, visitor)
+ reportIncident(context, visitor)
} else if (visitor.returned) {
// if the visited method returns the passed-in unsafe Intent, add this call to track it.
instances.add(call)
@@ -401,6 +458,113 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
}
}
+ /** Returns if the expression is evaluated to a protected broadcast action. */
+ private fun isProtectedBroadcastAction(expression: UExpression?): Boolean {
+ return BroadcastReceiverUtils.isProtectedBroadcast(
+ ConstantEvaluator().allowFieldInitializers().evaluate(expression) as String
+ )
+ }
+
+ /**
+ * Check if the call is within a branch of code that is protected by a protected broadcast
+ * action. If could either be an if statement that checks if the action of the intent is equal
+ * to a protected action; or an equivalent of a switch case statement.
+ */
+ private fun inProtectedBroadcastBranch(
+ context: JavaContext,
+ call: UCallExpression,
+ reference: UElement
+ ): Boolean {
+ return inProtectedBroadcastIfBranch(context, call, reference) ||
+ inProtectedBroadcastSwitchCase(call, reference)
+ }
+
+ private fun inProtectedBroadcastIfBranch(
+ context: JavaContext,
+ call: UCallExpression,
+ reference: UElement
+ ): Boolean {
+ var ifExp = call.getParentOfType<UIfExpression>()
+ while (ifExp != null) {
+ var op1: UExpression? = null
+ var op2: UExpression? = null
+ val condition = ifExp.condition
+ if (condition is UBinaryExpression && condition.operator === UastBinaryOperator.EQUALS) {
+ // handle kotlin ==
+ op1 = condition.leftOperand
+ op2 = condition.rightOperand
+ } else if (condition is UQualifiedReferenceExpression) {
+ // handle java equals method.
+ val methodCall = condition.selector as? UCallExpression
+ if (methodCall?.methodName == "equals") {
+ op1 = condition.receiver
+ op2 = methodCall.valueArguments[0]
+ }
+ }
+ if (
+ op1 != null &&
+ op2 != null &&
+ ((isIntentAction(op1, reference) && isProtectedBroadcastAction(op2)) ||
+ (isIntentAction(op2, reference) && isProtectedBroadcastAction(op1)))
+ ) {
+ return context.getLocation(call) in context.getLocation(ifExp.thenExpression)
+ }
+ ifExp = ifExp.getParentOfType()
+ }
+ return false
+ }
+
+ private fun inProtectedBroadcastSwitchCase(
+ call: UCallExpression,
+ reference: UElement
+ ): Boolean {
+ var switchExp = call.getParentOfType<USwitchExpression>()
+ while (switchExp != null) {
+ val subject = switchExp.expression as? UReferenceExpression
+ val caseExpression = call.getParentOfType<USwitchClauseExpression>() ?: return false
+ val caseValue = caseExpression.caseValues.firstOrNull() ?: return false
+ if ((caseValue.sourcePsi as? PsiSwitchLabelStatementBase)?.isDefaultCase == true)
+ return false
+ if (isIntentAction(subject, reference) && isProtectedBroadcastAction(caseValue)) return true
+ switchExp = switchExp.getParentOfType()
+ }
+ return false
+ }
+
+ private fun isIntentAction(expression: UExpression?, intentRef: UElement): Boolean {
+ val actionAssignmentCall = findIntentActionAssignmentCall(expression)
+ return actionAssignmentCall?.receiver?.skipParenthesizedExprDown()?.tryResolve() ===
+ intentRef.tryResolve()
+ }
+
+ private fun findIntentActionAssignmentCall(expression: UExpression?): UCallExpression? {
+ val actionExpr = expression?.skipParenthesizedExprDown()
+ val resolved = actionExpr?.tryResolve()
+
+ if (resolved is PsiVariable) {
+ val assignment = findLastAssignment(resolved, actionExpr) ?: return null
+ return findIntentActionAssignmentCall(assignment)
+ }
+
+ if (actionExpr is UQualifiedReferenceExpression) {
+ val call = actionExpr.selector as? UCallExpression ?: return null
+ return if (isReturningContext(call)) {
+ // eg. intent.apply { setAction("abc") } --> use filter variable.
+ findIntentActionAssignmentCall(actionExpr.receiver)
+ } else {
+ // eg. intent.getAction("abc") --> use getAction("abc") UCallExpression.
+ findIntentActionAssignmentCall(call)
+ }
+ }
+
+ val method = resolved as? PsiMethod ?: return null
+ return if ("getAction" == method.name) {
+ actionExpr as? UCallExpression
+ } else {
+ null
+ }
+ }
+
/**
* Handles kotlin scoping function with "this" object reference, like run, with. If "this" is
* tracked, get into the lambda function and keep track of "this".
@@ -420,7 +584,7 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
)
lambda.body.accept(visitor)
if (visitor.launched) {
- storeIncidentsToPartialResults(context, visitor)
+ reportIncident(context, visitor)
}
}
}
@@ -488,7 +652,7 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
if (call.methodName in registerReceiverMethods) {
val method = call.resolve() ?: return
- if (!isRuntimeReceiverProtected(call, method)) {
+ if (!isRuntimeReceiverProtected(call, method, context.evaluator)) {
result = true
}
}
@@ -548,7 +712,8 @@ class UnsafeIntentLaunchDetector : Detector(), SourceCodeScanner, XmlScanner {
private val IMPLEMENTATION =
Implementation(
UnsafeIntentLaunchDetector::class.java,
- EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST)
+ EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST),
+ Scope.JAVA_FILE_SCOPE,
)
/** Issue describing the problem and pointing to the detector implementation. */