Skip to content

Commit 0621e65

Browse files
luchua-bcsmowton
authored andcommitted
Query to detect exposure of sensitive information from android file intent
1 parent d0b307e commit 0621e65

20 files changed

+2173
-2
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/** Provides Android sink models related to file creation. */
2+
3+
import java
4+
import semmle.code.java.dataflow.DataFlow
5+
import semmle.code.java.dataflow.ExternalFlow
6+
import semmle.code.java.frameworks.android.Android
7+
import semmle.code.java.frameworks.android.Intent
8+
9+
/** A sink representing methods creating a file in Android. */
10+
class AndroidFileSink extends DataFlow::Node {
11+
AndroidFileSink() { sinkNode(this, "create-file") }
12+
}
13+
14+
/**
15+
* The Android class `android.os.AsyncTask` for running tasks off the UI thread to achieve
16+
* better user experience.
17+
*/
18+
class AsyncTask extends RefType {
19+
AsyncTask() { this.hasQualifiedName("android.os", "AsyncTask") }
20+
}
21+
22+
/** The `execute` method of Android `AsyncTask`. */
23+
class AsyncTaskExecuteMethod extends Method {
24+
AsyncTaskExecuteMethod() {
25+
this.getDeclaringType().getSourceDeclaration().getASourceSupertype*() instanceof AsyncTask and
26+
this.getName() = "execute"
27+
}
28+
29+
int getParamIndex() { result = 0 }
30+
}
31+
32+
/** The `executeOnExecutor` method of Android `AsyncTask`. */
33+
class AsyncTaskExecuteOnExecutorMethod extends Method {
34+
AsyncTaskExecuteOnExecutorMethod() {
35+
this.getDeclaringType().getSourceDeclaration().getASourceSupertype*() instanceof AsyncTask and
36+
this.getName() = "executeOnExecutor"
37+
}
38+
39+
int getParamIndex() { result = 1 }
40+
}
41+
42+
/** The `doInBackground` method of Android `AsyncTask`. */
43+
class AsyncTaskRunInBackgroundMethod extends Method {
44+
AsyncTaskRunInBackgroundMethod() {
45+
this.getDeclaringType().getSourceDeclaration().getASourceSupertype*() instanceof AsyncTask and
46+
this.getName() = "doInBackground"
47+
}
48+
}
49+
50+
/** The service start method of Android context. */
51+
class ContextStartServiceMethod extends Method {
52+
ContextStartServiceMethod() {
53+
this.getName() = ["startService", "startForegroundService"] and
54+
this.getDeclaringType().getASupertype*() instanceof TypeContext
55+
}
56+
}
57+
58+
/** The `onStartCommand` method of Android service. */
59+
class ServiceOnStartCommandMethod extends Method {
60+
ServiceOnStartCommandMethod() {
61+
this.hasName("onStartCommand") and
62+
this.getDeclaringType() instanceof AndroidService
63+
}
64+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/** Provides summary models relating to file content inputs of Android. */
2+
3+
import java
4+
import semmle.code.java.dataflow.FlowSources
5+
import semmle.code.java.frameworks.android.Android
6+
7+
/** The `startActivityForResult` method of Android `Activity`. */
8+
class StartActivityForResultMethod extends Method {
9+
StartActivityForResultMethod() {
10+
this.getDeclaringType().getASupertype*() instanceof AndroidActivity and
11+
this.getName() = "startActivityForResult"
12+
}
13+
}
14+
15+
/** Android class instance of `GET_CONTENT` intent. */
16+
class GetContentIntent extends ClassInstanceExpr {
17+
GetContentIntent() {
18+
this.getConstructedType().getASupertype*() instanceof TypeIntent and
19+
this.getArgument(0).(CompileTimeConstantExpr).getStringValue() =
20+
"android.intent.action.GET_CONTENT"
21+
or
22+
exists(Field f |
23+
this.getArgument(0) = f.getAnAccess() and
24+
f.hasName("ACTION_GET_CONTENT") and
25+
f.getDeclaringType() instanceof TypeIntent
26+
)
27+
}
28+
}
29+
30+
/** Android intent data model in the new CSV format. */
31+
private class AndroidIntentDataModel extends SummaryModelCsv {
32+
override predicate row(string row) {
33+
row =
34+
[
35+
"android.content;Intent;true;addCategory;;;Argument[-1];ReturnValue;taint",
36+
"android.content;Intent;true;addFlags;;;Argument[-1];ReturnValue;taint",
37+
"android.content;Intent;true;createChooser;;;Argument[0];ReturnValue;taint",
38+
"android.content;Intent;true;getData;;;Argument[-1];ReturnValue;taint",
39+
"android.content;Intent;true;getDataString;;;Argument[-1];ReturnValue;taint",
40+
"android.content;Intent;true;getExtras;;;Argument[-1];ReturnValue;taint",
41+
"android.content;Intent;true;getIntent;;;Argument[-1];ReturnValue;taint",
42+
"android.content;Intent;true;get" +
43+
[
44+
"ParcelableArray", "ParcelableArrayList", "Parcelable", "Serializable", "StringArray",
45+
"StringArrayList", "String"
46+
] + "Extra;;;Argument[-1..1];ReturnValue;taint",
47+
"android.content;Intent;true;put" +
48+
[
49+
"", "CharSequenceArrayList", "IntegerArrayList", "ParcelableArrayList",
50+
"StringArrayList"
51+
] + "Extra;;;Argument[1];Argument[-1];taint",
52+
"android.content;Intent;true;putExtras;;;Argument[1];Argument[-1];taint",
53+
"android.content;Intent;true;setData;;;Argument[0];ReturnValue;taint",
54+
"android.content;Intent;true;setDataAndType;;;Argument[-1];ReturnValue;taint",
55+
"android.content;Intent;true;setFlags;;;Argument[-1];ReturnValue;taint",
56+
"android.content;Intent;true;setType;;;Argument[-1];ReturnValue;taint",
57+
"android.net;Uri;true;getEncodedPath;;;Argument[-1];ReturnValue;taint",
58+
"android.net;Uri;true;getEncodedQuery;;;Argument[-1];ReturnValue;taint",
59+
"android.net;Uri;true;getLastPathSegment;;;Argument[-1];ReturnValue;taint",
60+
"android.net;Uri;true;getPath;;;Argument[-1];ReturnValue;taint",
61+
"android.net;Uri;true;getPathSegments;;;Argument[-1];ReturnValue;taint",
62+
"android.net;Uri;true;getQuery;;;Argument[-1];ReturnValue;taint",
63+
"android.net;Uri;true;getQueryParameter;;;Argument[-1];ReturnValue;taint",
64+
"android.net;Uri;true;getQueryParameters;;;Argument[-1];ReturnValue;taint",
65+
"android.os;AsyncTask;true;execute;;;Argument[0];ReturnValue;taint",
66+
"android.os;AsyncTask;true;doInBackground;;;Argument[0];ReturnValue;taint"
67+
]
68+
}
69+
}
70+
71+
/** Taint configuration for getting content intent. */
72+
class GetContentIntentConfig extends TaintTracking::Configuration {
73+
GetContentIntentConfig() { this = "GetContentIntentConfig" }
74+
75+
override predicate isSource(DataFlow::Node src) {
76+
exists(GetContentIntent gi | src.asExpr() = gi)
77+
}
78+
79+
override predicate isSink(DataFlow::Node sink) {
80+
exists(MethodAccess ma |
81+
ma.getMethod() instanceof StartActivityForResultMethod and sink.asExpr() = ma.getArgument(0)
82+
)
83+
}
84+
}
85+
86+
/** Android `Intent` input to request file loading. */
87+
class AndroidFileIntentInput extends LocalUserInput {
88+
MethodAccess ma;
89+
90+
AndroidFileIntentInput() {
91+
this.asExpr() = ma.getArgument(0) and
92+
ma.getMethod() instanceof StartActivityForResultMethod and
93+
exists(GetContentIntentConfig cc, GetContentIntent gi |
94+
cc.hasFlow(DataFlow::exprNode(gi), DataFlow::exprNode(ma.getArgument(0)))
95+
)
96+
}
97+
98+
/** The request code identifying a specific intent, which is to be matched in `onActivityResult()`. */
99+
int getRequestCode() { result = ma.getArgument(1).(CompileTimeConstantExpr).getIntValue() }
100+
}
101+
102+
/** The `onActivityForResult` method of Android `Activity` */
103+
class OnActivityForResultMethod extends Method {
104+
OnActivityForResultMethod() {
105+
this.getDeclaringType().getASupertype*() instanceof AndroidActivity and
106+
this.getName() = "onActivityResult"
107+
}
108+
}
109+
110+
/** Input of Android activity result from the same application or another application. */
111+
class AndroidActivityResultInput extends DataFlow::Node {
112+
OnActivityForResultMethod m;
113+
114+
AndroidActivityResultInput() { this.asExpr() = m.getParameter(2).getAnAccess() }
115+
116+
/** The request code matching a specific intent request. */
117+
VarAccess getRequestCodeVar() { result = m.getParameter(0).getAnAccess() }
118+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
public class LoadFileFromAppActivity extends Activity {
2+
public static final int REQUEST_CODE__SELECT_CONTENT_FROM_APPS = 99;
3+
4+
@Override
5+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
6+
if (requestCode == LoadFileFromAppActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS &&
7+
resultCode == RESULT_OK) {
8+
9+
{
10+
// BAD: Load file without validation
11+
loadOfContentFromApps(data, resultCode);
12+
}
13+
14+
{
15+
// GOOD: load file with validation
16+
if (!data.getData().getPath().startsWith("/data/data")) {
17+
loadOfContentFromApps(data, resultCode);
18+
}
19+
}
20+
}
21+
}
22+
23+
private void loadOfContentFromApps(Intent contentIntent, int resultCode) {
24+
Uri streamsToUpload = contentIntent.getData();
25+
try {
26+
RandomAccessFile file = new RandomAccessFile(streamsToUpload.getPath(), "r");
27+
} catch (Exception ex) {
28+
ex.printStackTrace();
29+
}
30+
}
31+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>The Android API allows to start an activity in another mobile application and receive a result back.
7+
When starting an activity to retrieve a file from another application, missing input validation can
8+
lead to leaking of sensitive configuration file or user data because the intent is from the application
9+
itself that is allowed to access its protected data therefore bypassing the access control.
10+
</p>
11+
</overview>
12+
13+
<recommendation>
14+
<p>
15+
When loading file data from an activity of another application, validate that the file path is not its own
16+
protected directory, which is a subdirectory of the Android application directory <code>/data/data/</code>.
17+
</p>
18+
</recommendation>
19+
20+
<example>
21+
<p>
22+
The following examples show the bad situation and the good situation respectively. In bad situation, a
23+
file is loaded without path validation. In good situation, a file is loaded with path validation.
24+
</p>
25+
<sample src="LoadFileFromAppActivity.java" />
26+
</example>
27+
28+
<references>
29+
<li>
30+
Google:
31+
<a href="https://developer.android.com/training/basics/intents">Android: Interacting with Other Apps</a>.
32+
</li>
33+
<li>
34+
CVE:
35+
<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>.
36+
</li>
37+
</references>
38+
</qhelp>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @name Leaking sensitive Android file
3+
* @description Getting file intent from user input without path validation could leak arbitrary
4+
* Android configuration file and sensitive user data.
5+
* @kind path-problem
6+
* @id java/sensitive_android_file_leak
7+
* @tags security
8+
* external/cwe/cwe-200
9+
*/
10+
11+
import java
12+
import AndroidFileIntentSink
13+
import AndroidFileIntentSource
14+
import DataFlow2::PathGraph
15+
import semmle.code.java.dataflow.TaintTracking2
16+
17+
class AndroidFileLeakConfig extends TaintTracking2::Configuration {
18+
AndroidFileLeakConfig() { this = "AndroidFileLeakConfig" }
19+
20+
/** Holds if it is an access to file intent result. */
21+
override predicate isSource(DataFlow2::Node src) {
22+
exists(
23+
AndroidActivityResultInput ai, AndroidFileIntentInput fi, IfStmt ifs, VarAccess intentVar // if (requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS)
24+
|
25+
ifs.getCondition().getAChildExpr().getAChildExpr().(CompileTimeConstantExpr).getIntValue() =
26+
fi.getRequestCode() and
27+
ifs.getCondition().getAChildExpr().getAChildExpr() = ai.getRequestCodeVar() and
28+
intentVar.getType() instanceof TypeIntent and
29+
intentVar.(Argument).getAnEnclosingStmt() = ifs.getThen() and
30+
src.asExpr() = intentVar
31+
)
32+
}
33+
34+
/** Holds if it is a sink of file access in Android. */
35+
override predicate isSink(DataFlow2::Node sink) { sink instanceof AndroidFileSink }
36+
37+
override predicate isAdditionalTaintStep(DataFlow2::Node prev, DataFlow2::Node succ) {
38+
exists(MethodAccess aema, AsyncTaskRunInBackgroundMethod arm |
39+
// fileAsyncTask.execute(params) will invoke doInBackground(params) of FileAsyncTask
40+
aema.getQualifier().getType() = arm.getDeclaringType() and
41+
(
42+
aema.getMethod() instanceof AsyncTaskExecuteMethod and
43+
prev.asExpr() = aema.getArgument(0)
44+
or
45+
aema.getMethod() instanceof AsyncTaskExecuteOnExecutorMethod and
46+
prev.asExpr() = aema.getArgument(1)
47+
) and
48+
succ.asExpr() = arm.getParameter(0).getAnAccess()
49+
)
50+
or
51+
exists(MethodAccess csma, ServiceOnStartCommandMethod ssm, ClassInstanceExpr ce |
52+
csma.getMethod() instanceof ContextStartServiceMethod and
53+
ce.getConstructedType() instanceof TypeIntent and // Intent intent = new Intent(context, FileUploader.class);
54+
ce.getArgument(1).getType().(ParameterizedType).getTypeArgument(0) = ssm.getDeclaringType() and
55+
DataFlow2::localExprFlow(ce, csma.getArgument(0)) and // context.startService(intent);
56+
prev.asExpr() = csma.getArgument(0) and
57+
succ.asExpr() = ssm.getParameter(0).getAnAccess() // public int onStartCommand(Intent intent, int flags, int startId) {...} in FileUploader
58+
)
59+
}
60+
61+
override predicate isSanitizer(DataFlow2::Node node) {
62+
exists(
63+
MethodAccess startsWith // "startsWith" path check
64+
|
65+
startsWith.getMethod().hasName("startsWith") and
66+
(
67+
DataFlow2::localExprFlow(node.asExpr(), startsWith.getQualifier()) or
68+
DataFlow2::localExprFlow(node.asExpr(),
69+
startsWith.getQualifier().(MethodAccess).getQualifier())
70+
)
71+
)
72+
}
73+
}
74+
75+
from DataFlow2::PathNode source, DataFlow2::PathNode sink, AndroidFileLeakConfig conf
76+
where conf.hasFlowPath(source, sink)
77+
select sink.getNode(), source, sink, "Leaking arbitrary Android file from $@.", source.getNode(),
78+
"this user input"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import java.io.FileOutputStream;
2+
3+
import android.app.Service;
4+
import android.content.Intent;
5+
import android.net.Uri;
6+
import android.os.Bundle;
7+
import android.os.AsyncTask;
8+
9+
public class FileService extends Service {
10+
public static String KEY_LOCAL_FILE = "local_file";
11+
/**
12+
* Service initialization
13+
*/
14+
@Override
15+
public void onCreate() {
16+
super.onCreate();
17+
}
18+
19+
@Override
20+
public int onStartCommand(Intent intent, int flags, int startId) {
21+
String localPath = intent.getStringExtra(KEY_LOCAL_FILE);
22+
CopyAndUploadContentUrisTask copyTask = new CopyAndUploadContentUrisTask();
23+
24+
copyTask.execute(
25+
copyTask.makeParamsToExecute(localPath)
26+
);
27+
return 2;
28+
}
29+
30+
public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, String> {
31+
public Object[] makeParamsToExecute(
32+
String sourceUri
33+
) {
34+
return new Object[] {
35+
sourceUri
36+
};
37+
}
38+
39+
@Override
40+
protected String doInBackground(Object[] params) {
41+
FileOutputStream outputStream = null;
42+
43+
try {
44+
String[] uris = (String[]) params[1];
45+
outputStream = new FileOutputStream(uris[0]);
46+
return "success";
47+
} catch (Exception e) {
48+
}
49+
return "failure";
50+
}
51+
52+
@Override
53+
protected void onPostExecute(String result) {
54+
}
55+
56+
@Override
57+
protected void onPreExecute() {
58+
}
59+
60+
@Override
61+
protected void onProgressUpdate(Void... values) {
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)