mirror of
https://github.com/github/codeql.git
synced 2025-12-21 19:26:31 +01:00
Query to detect exposure of sensitive information from android file intent
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user