Skip to content

Commit de1ecf9

Browse files
authored
Merge pull request github#11915 from egregius313/egregius313/arbitrary-apk-installation
Java: Arbitrary APK installation
2 parents dd0723c + 59eea2a commit de1ecf9

12 files changed

+459
-1
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/** Provide classes to reason about Android Intents that can install APKs. */
2+
3+
import java
4+
import semmle.code.java.frameworks.android.Intent
5+
import semmle.code.java.dataflow.DataFlow
6+
private import semmle.code.java.dataflow.ExternalFlow
7+
private import semmle.code.java.dataflow.FlowSources
8+
9+
/** A string literal that represents the MIME type for Android APKs. */
10+
class PackageArchiveMimeTypeLiteral extends StringLiteral {
11+
PackageArchiveMimeTypeLiteral() { this.getValue() = "application/vnd.android.package-archive" }
12+
}
13+
14+
/** The `android.content.Intent.ACTION_INSTALL_PACKAGE` constant. */
15+
class InstallPackageAction extends Expr {
16+
InstallPackageAction() {
17+
this.(StringLiteral).getValue() = "android.intent.action.INSTALL_PACKAGE"
18+
or
19+
exists(VarAccess va |
20+
va.getVariable().hasName("ACTION_INSTALL_PACKAGE") and
21+
va.getQualifier().getType() instanceof TypeIntent
22+
)
23+
}
24+
}
25+
26+
/** A method that sets the MIME type of an intent. */
27+
class SetTypeMethod extends Method {
28+
SetTypeMethod() {
29+
this.hasName(["setType", "setTypeAndNormalize"]) and
30+
this.getDeclaringType() instanceof TypeIntent
31+
}
32+
}
33+
34+
/** A method that sets the data URI and the MIME type of an intent. */
35+
class SetDataAndTypeMethod extends Method {
36+
SetDataAndTypeMethod() {
37+
this.hasName(["setDataAndType", "setDataAndTypeAndNormalize"]) and
38+
this.getDeclaringType() instanceof TypeIntent
39+
}
40+
}
41+
42+
/** A method that sets the data URI of an intent. */
43+
class SetDataMethod extends Method {
44+
SetDataMethod() {
45+
this.hasName(["setData", "setDataAndNormalize", "setDataAndType", "setDataAndTypeAndNormalize"]) and
46+
this.getDeclaringType() instanceof TypeIntent
47+
}
48+
}
49+
50+
/** A dataflow sink for the URI of an intent. */
51+
class SetDataSink extends DataFlow::ExprNode {
52+
SetDataSink() {
53+
exists(MethodAccess ma |
54+
this.getExpr() = ma.getQualifier() and
55+
ma.getMethod() instanceof SetDataMethod
56+
)
57+
}
58+
}
59+
60+
/** A method that generates a URI. */
61+
class UriConstructorMethod extends Method {
62+
UriConstructorMethod() {
63+
this.hasQualifiedName("android.net", "Uri", ["fromFile", "fromParts"]) or
64+
this.hasQualifiedName("androidx.core.content", "FileProvider", "getUriForFile")
65+
}
66+
}
67+
68+
/**
69+
* A dataflow source representing the URIs which an APK not controlled by the
70+
* application may come from. Including external storage and web URLs.
71+
*/
72+
class ExternalApkSource extends DataFlow::Node {
73+
ExternalApkSource() {
74+
sourceNode(this, "android-external-storage-dir") or
75+
this.asExpr().(MethodAccess).getMethod() instanceof UriConstructorMethod or
76+
this.asExpr().(StringLiteral).getValue().matches("file://%") or
77+
this instanceof RemoteFlowSource
78+
}
79+
}
80+
81+
/** The `setAction` method of the `android.content.Intent` class. */
82+
class SetActionMethod extends Method {
83+
SetActionMethod() {
84+
this.hasName("setAction") and
85+
this.getDeclaringType() instanceof TypeIntent
86+
}
87+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/** Provides dataflow configurations to reason about installation of arbitrary Android APKs. */
2+
3+
import java
4+
import semmle.code.java.dataflow.DataFlow
5+
import semmle.code.java.dataflow.TaintTracking
6+
private import semmle.code.java.security.ArbitraryApkInstallation
7+
8+
/**
9+
* A dataflow configuration for flow from an external source of an APK to the
10+
* `setData[AndType][AndNormalize]` method of an intent.
11+
*/
12+
private module ApkInstallationConfiguration implements DataFlow::ConfigSig {
13+
predicate isSource(DataFlow::Node node) { node instanceof ExternalApkSource }
14+
15+
predicate isSink(DataFlow::Node node) {
16+
exists(MethodAccess ma |
17+
ma.getMethod() instanceof SetDataMethod and
18+
ma.getArgument(0) = node.asExpr() and
19+
(
20+
PackageArchiveMimeTypeFlow::hasFlowToExpr(ma.getQualifier())
21+
or
22+
InstallPackageActionFlow::hasFlowToExpr(ma.getQualifier())
23+
)
24+
)
25+
}
26+
}
27+
28+
module ApkInstallationFlow = DataFlow::Make<ApkInstallationConfiguration>;
29+
30+
private newtype ActionState =
31+
ActionUnset() or
32+
HasInstallPackageAction()
33+
34+
/**
35+
* A dataflow configuration tracking the flow from the `android.content.Intent.ACTION_INSTALL_PACKAGE`
36+
* constant to either the constructor of an intent or the `setAction` method of an intent.
37+
*
38+
* This is used to track if an intent is used to install an APK.
39+
*/
40+
private module InstallPackageActionConfiguration implements DataFlow::StateConfigSig {
41+
class FlowState = ActionState;
42+
43+
predicate isSource(DataFlow::Node source, FlowState state) {
44+
source.asExpr() instanceof InstallPackageAction and state instanceof ActionUnset
45+
}
46+
47+
predicate isAdditionalFlowStep(
48+
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
49+
) {
50+
state1 instanceof ActionUnset and
51+
state2 instanceof HasInstallPackageAction and
52+
(
53+
exists(ConstructorCall cc |
54+
cc.getConstructedType() instanceof TypeIntent and
55+
node1.asExpr() = cc.getArgument(0) and
56+
node1.asExpr().getType() instanceof TypeString and
57+
node2.asExpr() = cc
58+
)
59+
or
60+
exists(MethodAccess ma |
61+
ma.getMethod() instanceof SetActionMethod and
62+
node1.asExpr() = ma.getArgument(0) and
63+
node2.asExpr() = ma.getQualifier()
64+
)
65+
)
66+
}
67+
68+
predicate isSink(DataFlow::Node node, FlowState state) {
69+
state instanceof HasInstallPackageAction and node.asExpr().getType() instanceof TypeIntent
70+
}
71+
72+
predicate isBarrier(DataFlow::Node node, FlowState state) { none() }
73+
}
74+
75+
private module InstallPackageActionFlow =
76+
TaintTracking::MakeWithState<InstallPackageActionConfiguration>;
77+
78+
private newtype MimeTypeState =
79+
MimeTypeUnset() or
80+
HasPackageArchiveMimeType()
81+
82+
/**
83+
* A dataflow configuration tracking the flow of the Android APK MIME type to
84+
* the `setType` or `setTypeAndNormalize` method of an intent, followed by a call
85+
* to `setData[AndType][AndNormalize]`.
86+
*/
87+
private module PackageArchiveMimeTypeConfiguration implements DataFlow::StateConfigSig {
88+
class FlowState = MimeTypeState;
89+
90+
predicate isSource(DataFlow::Node node, FlowState state) {
91+
node.asExpr() instanceof PackageArchiveMimeTypeLiteral and
92+
state instanceof MimeTypeUnset
93+
}
94+
95+
predicate isAdditionalFlowStep(
96+
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
97+
) {
98+
state1 instanceof MimeTypeUnset and
99+
state2 instanceof HasPackageArchiveMimeType and
100+
exists(MethodAccess ma |
101+
ma.getQualifier() = node2.asExpr() and
102+
(
103+
ma.getMethod() instanceof SetTypeMethod and
104+
ma.getArgument(0) = node1.asExpr()
105+
or
106+
ma.getMethod() instanceof SetDataAndTypeMethod and
107+
ma.getArgument(1) = node1.asExpr()
108+
)
109+
)
110+
}
111+
112+
predicate isSink(DataFlow::Node node, FlowState state) {
113+
state instanceof HasPackageArchiveMimeType and
114+
node instanceof SetDataSink
115+
}
116+
117+
predicate isBarrier(DataFlow::Node node, FlowState state) { none() }
118+
}
119+
120+
private module PackageArchiveMimeTypeFlow =
121+
TaintTracking::MakeWithState<PackageArchiveMimeTypeConfiguration>;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
Android allows an application to install an Android Package Kit (APK)
8+
using an <code>Intent</code> with
9+
the <code>"application/vnd.android.package-archive"</code> MIME type. If
10+
the file used in the <code>Intent</code> is from a location that is not
11+
controlled by the application (for example, an SD card that is
12+
universally writable), this can result in the unintended installation of untrusted applications.
13+
</p>
14+
</overview>
15+
16+
<recommendation>
17+
<p>
18+
You should install packages using
19+
the <code>PackageInstaller</code> class.
20+
</p>
21+
22+
<p>
23+
If you need to install from a file, you should use
24+
a <code>FileProvider</code>. Content providers can provide more specific
25+
permissions than file system permissions can.
26+
</p>
27+
28+
<p>
29+
When your application does not require package installations, do not add
30+
the <code>REQUEST_INSTALL_PACKAGES</code> permission in the manifest file.
31+
</p>
32+
</recommendation>
33+
34+
<example>
35+
36+
<p>
37+
In the following (bad) example, the package is installed from a file which
38+
may be altered by another application:
39+
</p>
40+
41+
<sample src="InstallApkWithFile.java"/>
42+
43+
<p>
44+
In the following (good) example, the package is installed by using
45+
a <code>FileProvider</code>:
46+
</p>
47+
48+
<sample src="InstallApkWithFileProvider.java"/>
49+
50+
<p>
51+
In the following (good) example, the package is installed using an
52+
instance of the <code>android.content.pm.PackageInstaller</code> class:
53+
</p>
54+
55+
<sample src="InstallApkWithPackageInstaller.java"/>
56+
</example>
57+
58+
<references>
59+
<li>
60+
Android Developers: <a href="https://developer.android.com/reference/android/content/Intent#ACTION_INSTALL_PACKAGE">Intent.ACTION_INSTALL_PACKAGE</a>.
61+
</li>
62+
<li>
63+
Android Developers: <a href="https://developer.android.com/reference/android/Manifest.permission#REQUEST_INSTALL_PACKAGES">Manifest.permission.REQUEST_INSTALL_PACKAGES</a>.
64+
</li>
65+
<li>
66+
Android Developers: <a href="https://developer.android.com/reference/android/content/pm/PackageInstaller">PackageInstaller</a>.
67+
</li>
68+
<li>
69+
Android Developers: <a href="https://developer.android.com/reference/androidx/core/content/FileProvider">FileProvider</a>.
70+
</li>
71+
</references>
72+
</qhelp>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @id java/android/arbitrary-apk-installation
3+
* @name Android APK installation
4+
* @description Creating an intent with a URI pointing to a untrusted file can lead to the installation of an untrusted application.
5+
* @kind path-problem
6+
* @security-severity 9.3
7+
* @problem.severity error
8+
* @precision medium
9+
* @tags security
10+
* external/cwe/cwe-094
11+
*/
12+
13+
import java
14+
import semmle.code.java.security.ArbitraryApkInstallationQuery
15+
import ApkInstallationFlow::PathGraph
16+
17+
from ApkInstallationFlow::PathNode source, ApkInstallationFlow::PathNode sink
18+
where ApkInstallationFlow::hasFlowPath(source, sink)
19+
select sink.getNode(), source, sink, "Arbitrary Android APK installation."
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import android.app.Activity;
2+
import android.content.Intent;
3+
import android.net.Uri;
4+
import android.os.Environment;
5+
6+
import java.io.File;
7+
8+
/* Get a file from external storage */
9+
File file = new File(Environment.getExternalStorageDirectory(), "myapp.apk");
10+
Intent intent = new Intent(Intent.ACTION_VIEW);
11+
/* Set the mimetype to APK */
12+
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
13+
14+
startActivity(intent);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import android.app.Activity;
2+
import android.content.Context;
3+
import android.content.Intent;
4+
import android.net.Uri;
5+
import androidx.core.content.FileProvider;
6+
7+
import java.io.File;
8+
import java.io.FileOutputStream;
9+
10+
String tempFilename = "temporary.apk";
11+
byte[] buffer = new byte[16384];
12+
13+
/* Copy application asset into temporary file */
14+
try (InputStream is = getAssets().open(assetName);
15+
FileOutputStream fout = openFileOutput(tempFilename, Context.MODE_PRIVATE)) {
16+
int n;
17+
while ((n=is.read(buffer)) >= 0) {
18+
fout.write(buffer, 0, n);
19+
}
20+
}
21+
22+
/* Expose temporary file with FileProvider */
23+
File toInstall = new File(this.getFilesDir(), tempFilename);
24+
Uri applicationUri = FileProvider.getUriForFile(this, "com.example.apkprovider", toInstall);
25+
26+
/* Create Intent and set data to APK file. */
27+
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
28+
intent.setData(applicationUri);
29+
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
30+
31+
startActivity(intent);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import android.content.Context;
2+
import android.content.Intent;
3+
import android.content.pm.PackageInstaller;
4+
5+
private static final String PACKAGE_INSTALLED_ACTION =
6+
"com.example.SESSION_API_PACKAGE_INSTALLED";
7+
8+
/* Create the package installer and session */
9+
PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
10+
PackageInstaller.SessionParams params =
11+
new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
12+
int sessionId = packageInstaller.createSession(params);
13+
session = packageInstaller.openSession(sessionId);
14+
15+
/* Load asset into session */
16+
try (OutputStream packageInSession = session.openWrite("package", 0, -1);
17+
InputStream is = getAssets().open(assetName)) {
18+
byte[] buffer = new byte[16384];
19+
int n;
20+
while ((n = is.read(buffer)) >= 0) {
21+
packageInSession.write(buffer, 0, n);
22+
}
23+
}
24+
25+
/* Create status receiver */
26+
Intent intent = new Intent(this, InstallApkSessionApi.class);
27+
intent.setAction(PACKAGE_INSTALLED_ACTION);
28+
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
29+
IntentSender statusReceiver = pendingIntent.getIntentSender();
30+
31+
/* Commit the session */
32+
session.commit(statusReceiver);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `java/android/arbitrary-apk-installation`, to detect installation of APKs from untrusted sources.
5+

0 commit comments

Comments
 (0)