Query to detect exposure of sensitive information from android file intent

This commit is contained in:
luchua-bc
2021-08-29 18:52:33 +00:00
committed by Chris Smowton
parent d0b307ecfb
commit 0621e65827
20 changed files with 2173 additions and 2 deletions

View File

@@ -0,0 +1,64 @@
/** Provides Android sink models related to file creation. */
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.ExternalFlow
import semmle.code.java.frameworks.android.Android
import semmle.code.java.frameworks.android.Intent
/** A sink representing methods creating a file in Android. */
class AndroidFileSink extends DataFlow::Node {
AndroidFileSink() { sinkNode(this, "create-file") }
}
/**
* The Android class `android.os.AsyncTask` for running tasks off the UI thread to achieve
* better user experience.
*/
class AsyncTask extends RefType {
AsyncTask() { this.hasQualifiedName("android.os", "AsyncTask") }
}
/** The `execute` method of Android `AsyncTask`. */
class AsyncTaskExecuteMethod extends Method {
AsyncTaskExecuteMethod() {
this.getDeclaringType().getSourceDeclaration().getASourceSupertype*() instanceof AsyncTask and
this.getName() = "execute"
}
int getParamIndex() { result = 0 }
}
/** The `executeOnExecutor` method of Android `AsyncTask`. */
class AsyncTaskExecuteOnExecutorMethod extends Method {
AsyncTaskExecuteOnExecutorMethod() {
this.getDeclaringType().getSourceDeclaration().getASourceSupertype*() instanceof AsyncTask and
this.getName() = "executeOnExecutor"
}
int getParamIndex() { result = 1 }
}
/** The `doInBackground` method of Android `AsyncTask`. */
class AsyncTaskRunInBackgroundMethod extends Method {
AsyncTaskRunInBackgroundMethod() {
this.getDeclaringType().getSourceDeclaration().getASourceSupertype*() instanceof AsyncTask and
this.getName() = "doInBackground"
}
}
/** The service start method of Android context. */
class ContextStartServiceMethod extends Method {
ContextStartServiceMethod() {
this.getName() = ["startService", "startForegroundService"] and
this.getDeclaringType().getASupertype*() instanceof TypeContext
}
}
/** The `onStartCommand` method of Android service. */
class ServiceOnStartCommandMethod extends Method {
ServiceOnStartCommandMethod() {
this.hasName("onStartCommand") and
this.getDeclaringType() instanceof AndroidService
}
}

View File

@@ -0,0 +1,118 @@
/** Provides summary models relating to file content inputs of Android. */
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.frameworks.android.Android
/** The `startActivityForResult` method of Android `Activity`. */
class StartActivityForResultMethod extends Method {
StartActivityForResultMethod() {
this.getDeclaringType().getASupertype*() instanceof AndroidActivity and
this.getName() = "startActivityForResult"
}
}
/** Android class instance of `GET_CONTENT` intent. */
class GetContentIntent extends ClassInstanceExpr {
GetContentIntent() {
this.getConstructedType().getASupertype*() instanceof TypeIntent and
this.getArgument(0).(CompileTimeConstantExpr).getStringValue() =
"android.intent.action.GET_CONTENT"
or
exists(Field f |
this.getArgument(0) = f.getAnAccess() and
f.hasName("ACTION_GET_CONTENT") and
f.getDeclaringType() instanceof TypeIntent
)
}
}
/** Android intent data model in the new CSV format. */
private class AndroidIntentDataModel extends SummaryModelCsv {
override predicate row(string row) {
row =
[
"android.content;Intent;true;addCategory;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;addFlags;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;createChooser;;;Argument[0];ReturnValue;taint",
"android.content;Intent;true;getData;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;getDataString;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;getExtras;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;getIntent;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;get" +
[
"ParcelableArray", "ParcelableArrayList", "Parcelable", "Serializable", "StringArray",
"StringArrayList", "String"
] + "Extra;;;Argument[-1..1];ReturnValue;taint",
"android.content;Intent;true;put" +
[
"", "CharSequenceArrayList", "IntegerArrayList", "ParcelableArrayList",
"StringArrayList"
] + "Extra;;;Argument[1];Argument[-1];taint",
"android.content;Intent;true;putExtras;;;Argument[1];Argument[-1];taint",
"android.content;Intent;true;setData;;;Argument[0];ReturnValue;taint",
"android.content;Intent;true;setDataAndType;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;setFlags;;;Argument[-1];ReturnValue;taint",
"android.content;Intent;true;setType;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getEncodedPath;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getEncodedQuery;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getLastPathSegment;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getPath;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getPathSegments;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getQuery;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getQueryParameter;;;Argument[-1];ReturnValue;taint",
"android.net;Uri;true;getQueryParameters;;;Argument[-1];ReturnValue;taint",
"android.os;AsyncTask;true;execute;;;Argument[0];ReturnValue;taint",
"android.os;AsyncTask;true;doInBackground;;;Argument[0];ReturnValue;taint"
]
}
}
/** Taint configuration for getting content intent. */
class GetContentIntentConfig extends TaintTracking::Configuration {
GetContentIntentConfig() { this = "GetContentIntentConfig" }
override predicate isSource(DataFlow::Node src) {
exists(GetContentIntent gi | src.asExpr() = gi)
}
override predicate isSink(DataFlow::Node sink) {
exists(MethodAccess ma |
ma.getMethod() instanceof StartActivityForResultMethod and sink.asExpr() = ma.getArgument(0)
)
}
}
/** Android `Intent` input to request file loading. */
class AndroidFileIntentInput extends LocalUserInput {
MethodAccess ma;
AndroidFileIntentInput() {
this.asExpr() = ma.getArgument(0) and
ma.getMethod() instanceof StartActivityForResultMethod and
exists(GetContentIntentConfig cc, GetContentIntent gi |
cc.hasFlow(DataFlow::exprNode(gi), DataFlow::exprNode(ma.getArgument(0)))
)
}
/** The request code identifying a specific intent, which is to be matched in `onActivityResult()`. */
int getRequestCode() { result = ma.getArgument(1).(CompileTimeConstantExpr).getIntValue() }
}
/** The `onActivityForResult` method of Android `Activity` */
class OnActivityForResultMethod extends Method {
OnActivityForResultMethod() {
this.getDeclaringType().getASupertype*() instanceof AndroidActivity and
this.getName() = "onActivityResult"
}
}
/** Input of Android activity result from the same application or another application. */
class AndroidActivityResultInput extends DataFlow::Node {
OnActivityForResultMethod m;
AndroidActivityResultInput() { this.asExpr() = m.getParameter(2).getAnAccess() }
/** The request code matching a specific intent request. */
VarAccess getRequestCodeVar() { result = m.getParameter(0).getAnAccess() }
}

View File

@@ -0,0 +1,31 @@
public class LoadFileFromAppActivity extends Activity {
public static final int REQUEST_CODE__SELECT_CONTENT_FROM_APPS = 99;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == LoadFileFromAppActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS &&
resultCode == RESULT_OK) {
{
// BAD: Load file without validation
loadOfContentFromApps(data, resultCode);
}
{
// GOOD: load file with validation
if (!data.getData().getPath().startsWith("/data/data")) {
loadOfContentFromApps(data, resultCode);
}
}
}
}
private void loadOfContentFromApps(Intent contentIntent, int resultCode) {
Uri streamsToUpload = contentIntent.getData();
try {
RandomAccessFile file = new RandomAccessFile(streamsToUpload.getPath(), "r");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

View File

@@ -0,0 +1,38 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>The Android API allows to start an activity in another mobile application and receive a result back.
When starting an activity to retrieve a file from another application, missing input validation can
lead to leaking of sensitive configuration file or user data because the intent is from the application
itself that is allowed to access its protected data therefore bypassing the access control.
</p>
</overview>
<recommendation>
<p>
When loading file data from an activity of another application, validate that the file path is not its own
protected directory, which is a subdirectory of the Android application directory <code>/data/data/</code>.
</p>
</recommendation>
<example>
<p>
The following examples show the bad situation and the good situation respectively. In bad situation, a
file is loaded without path validation. In good situation, a file is loaded with path validation.
</p>
<sample src="LoadFileFromAppActivity.java" />
</example>
<references>
<li>
Google:
<a href="https://developer.android.com/training/basics/intents">Android: Interacting with Other Apps</a>.
</li>
<li>
CVE:
<a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-32695">CVE-2021-32695: File Sharing Flow Initiated by a Victim Leaks Sensitive Data to a Malicious App</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,78 @@
/**
* @name Leaking sensitive Android file
* @description Getting file intent from user input without path validation could leak arbitrary
* Android configuration file and sensitive user data.
* @kind path-problem
* @id java/sensitive_android_file_leak
* @tags security
* external/cwe/cwe-200
*/
import java
import AndroidFileIntentSink
import AndroidFileIntentSource
import DataFlow2::PathGraph
import semmle.code.java.dataflow.TaintTracking2
class AndroidFileLeakConfig extends TaintTracking2::Configuration {
AndroidFileLeakConfig() { this = "AndroidFileLeakConfig" }
/** Holds if it is an access to file intent result. */
override predicate isSource(DataFlow2::Node src) {
exists(
AndroidActivityResultInput ai, AndroidFileIntentInput fi, IfStmt ifs, VarAccess intentVar // if (requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS)
|
ifs.getCondition().getAChildExpr().getAChildExpr().(CompileTimeConstantExpr).getIntValue() =
fi.getRequestCode() and
ifs.getCondition().getAChildExpr().getAChildExpr() = ai.getRequestCodeVar() and
intentVar.getType() instanceof TypeIntent and
intentVar.(Argument).getAnEnclosingStmt() = ifs.getThen() and
src.asExpr() = intentVar
)
}
/** Holds if it is a sink of file access in Android. */
override predicate isSink(DataFlow2::Node sink) { sink instanceof AndroidFileSink }
override predicate isAdditionalTaintStep(DataFlow2::Node prev, DataFlow2::Node succ) {
exists(MethodAccess aema, AsyncTaskRunInBackgroundMethod arm |
// fileAsyncTask.execute(params) will invoke doInBackground(params) of FileAsyncTask
aema.getQualifier().getType() = arm.getDeclaringType() and
(
aema.getMethod() instanceof AsyncTaskExecuteMethod and
prev.asExpr() = aema.getArgument(0)
or
aema.getMethod() instanceof AsyncTaskExecuteOnExecutorMethod and
prev.asExpr() = aema.getArgument(1)
) and
succ.asExpr() = arm.getParameter(0).getAnAccess()
)
or
exists(MethodAccess csma, ServiceOnStartCommandMethod ssm, ClassInstanceExpr ce |
csma.getMethod() instanceof ContextStartServiceMethod and
ce.getConstructedType() instanceof TypeIntent and // Intent intent = new Intent(context, FileUploader.class);
ce.getArgument(1).getType().(ParameterizedType).getTypeArgument(0) = ssm.getDeclaringType() and
DataFlow2::localExprFlow(ce, csma.getArgument(0)) and // context.startService(intent);
prev.asExpr() = csma.getArgument(0) and
succ.asExpr() = ssm.getParameter(0).getAnAccess() // public int onStartCommand(Intent intent, int flags, int startId) {...} in FileUploader
)
}
override predicate isSanitizer(DataFlow2::Node node) {
exists(
MethodAccess startsWith // "startsWith" path check
|
startsWith.getMethod().hasName("startsWith") and
(
DataFlow2::localExprFlow(node.asExpr(), startsWith.getQualifier()) or
DataFlow2::localExprFlow(node.asExpr(),
startsWith.getQualifier().(MethodAccess).getQualifier())
)
)
}
}
from DataFlow2::PathNode source, DataFlow2::PathNode sink, AndroidFileLeakConfig conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Leaking arbitrary Android file from $@.", source.getNode(),
"this user input"