Add Fragment injection query

This commit is contained in:
Tony Torralba
2021-10-20 12:03:23 +02:00
parent efb471687c
commit 701d12fb5b
12 changed files with 275 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import java
/** The class `android.app.Fragment` */
class Fragment extends Class {
Fragment() { this.hasQualifiedName("android.app", "Fragment") }
}
/** The method `instantiate` of the class `android.app.Fragment`. */
class FragmentInstantiateMethod extends Method {
FragmentInstantiateMethod() {
this.getDeclaringType() instanceof Fragment and
this.hasName("instantiate")
}
}

View File

@@ -0,0 +1,60 @@
import java
private import semmle.code.java.dataflow.TaintTracking
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.frameworks.android.Android
private import semmle.code.java.frameworks.android.Fragment
private import semmle.code.java.Reflection
/**
* A sink for Fragment injection vulnerabilities,
* that is, method calls that dynamically add Fragments to Activities.
*/
abstract class FragmentInjectionSink extends DataFlow::Node { }
/**
* A unit class for adding additional taint steps.
*
* Extend this class to add additional taint steps that should apply to `FragmentInjectionTaintConf`.
*/
class FragmentInjectionAdditionalTaintStep extends Unit {
abstract predicate step(DataFlow::Node n1, DataFlow::Node n2);
}
private class FragmentInjectionSinkModels extends SinkModelCsv {
override predicate row(string row) {
row =
["android.app", "android.support.v4.app", "androidx.fragment.app"] +
";FragmentTransaction;true;" +
[
"add;(Class,Bundle,String);;Argument[0]", "add;(Fragment,String);;Argument[0]",
"add;(int,Class,Bundle);;Argument[1]", "add;(int,Fragment);;Argument[1]",
"add;(int,Class,Bundle,String);;Argument[1]", "add;(int,Fragment,String);;Argument[1]",
"attach;(Fragment);;Argument[0]", "replace;(int,Class,Bundle);;Argument[1]",
"replace;(int,Fragment);;Argument[1]", "replace;(int,Class,Bundle,String);;Argument[1]",
"replace;(int,Fragment,String);;Argument[1]",
] + ";fragment-injection"
}
}
private class DefaultFragmentInjectionSink extends FragmentInjectionSink {
DefaultFragmentInjectionSink() { sinkNode(this, "fragment-injection") }
}
private class DefaultFragmentInjectionAdditionalTaintStep extends FragmentInjectionAdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(ReflectiveClassIdentifierMethodAccess ma |
ma.getArgument(0) = n1.asExpr() and ma = n2.asExpr()
)
or
exists(NewInstance ni |
ni.getQualifier() = n1.asExpr() and
ni = n2.asExpr()
)
or
exists(MethodAccess ma |
ma.getMethod() instanceof FragmentInstantiateMethod and
ma.getArgument(1) = n1.asExpr() and
ma = n2.asExpr()
)
}
}

View File

@@ -0,0 +1,20 @@
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.FragmentInjection
/**
* A taint-tracking configuration for unsafe user input
* that is used to create Android Fragments dinamically.
*/
class FragmentInjectionTaintConf extends TaintTracking::Configuration {
FragmentInjectionTaintConf() { this = "FragmentInjectionTaintConf" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof FragmentInjectionSink }
override predicate isAdditionalTaintStep(DataFlow::Node n1, DataFlow::Node n2) {
any(FragmentInjectionAdditionalTaintStep c).step(n1, n2)
}
}

View File

@@ -0,0 +1,60 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Fragments are reusable parts of an Android application's user interface.
Even though a Fragment controls its own lifecycle and layout and handles its input events,
it cannot exist on its own: it must be hosted either by an Activity or another fragment.
This means that a Fragment will be accessible by third-party applications (that is, exported)
only if its hosting Activity is itself exported.
</p>
<p>
If an exported Activity dinamically creates and hosts Fragments instantiated with externally
provided names, a malicious application could provide the name of an arbitrary Fragment, even
one not designed to be externally accessible, and inject it into the Activity, effectively
bypassing access controls and exposing the application to unintended effects.
</p>
</overview>
<recommendation>
<p>
In general, do not instantiate classes (including Fragments) with user-provided names
without proper validation.
Also, if an exported Activity is extending the <code>PreferenceActivity</code> class, make sure that
the <code>isValidFragment</code> method is overriden and only returns <code>true</code> when the provided
<code>fragmentName</code> points to an intended Fragment.
</p>
</recommendation>
<example>
<p>
The following example shows two cases: in the first one, untrusted data is used to instantiate and
add a Fragment to an Activity, while in the second one, a Fragment is safely added with a static name.
</p>
<sample src="FragmentInjection.java" />
<p>
The next example shows two Activities extending <code>PreferenceActivity</code>. The first one overrides
<code>isValidFragment</code>, but it wrongly returns <code>true</code> inconditionally. The second Activity
correctly overrides <code>isValidFragment</code> to only return <code>true</code> when <code>fragmentName</code>
is a trusted Fragment name.
</p>
<sample src="FragmentInjectionInPreferenceActivity.java" />
</example>
<references>
<li> Google Help:
<a href="https://support.google.com/faqs/answer/7188427?hl=en">How to fix Fragment Injection vulnerability</a>.
</li>
<li>
IBM Security Systems:
<a href="https://securityintelligence.com/wp-content/uploads/2013/12/android-collapses-into-fragments.pdf">Android collapses into Fragments</a>.
</li>
<li>
Android Developers:
<a href="https://developer.android.com/guide/fragments">Fragments</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,22 @@
public class MyActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstance) {
try {
super.onCreate(savedInstance);
// BAD: Fragment instantiated from user input
{
String fName = getIntent().getStringExtra("fragmentName");
getFragmentManager().beginTransaction().replace(com.android.internal.R.id.prefs,
Fragment.instantiate(this, fName, null)).commit();
}
// GOOD: Fragment instantiated statically
{
getFragmentManager().beginTransaction()
.replace(com.android.internal.R.id.prefs, new MyFragment()).commit();
}
} catch (Exception e) {
}
}
}

View File

@@ -0,0 +1,4 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<include src="FragmentInjection.inc.qhelp"></include>
</qhelp>

View File

@@ -0,0 +1,21 @@
/**
* @name Android Fragment injection
* @description Instantiating an Android Fragment from a user-provided value
* may lead to Fragment injection.
* @kind path-problem
* @problem.severity error
* @security-severity 9.8
* @precision high
* @id java/android/fragment-injection
* @tags security
* external/cwe/cwe-470
*/
import java
import semmle.code.java.security.FragmentInjectionQuery
import DataFlow::PathGraph
from DataFlow::PathNode source, DataFlow::PathNode sink
where any(FragmentInjectionTaintConf conf).hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Fragment injection from $@.", source.getNode(),
"this user input"

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
package="com.example.myapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,11 @@
import java
import semmle.code.java.security.FragmentInjectionQuery
import TestUtilities.InlineFlowTest
class Test extends InlineFlowTest {
override DataFlow::Configuration getValueFlowConfig() { none() }
override TaintTracking::Configuration getTaintFlowConfig() {
result instanceof FragmentInjectionTaintConf
}
}

View File

@@ -0,0 +1,39 @@
package com.example.myapp;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstance) {
try {
super.onCreate(savedInstance);
final String fname = getIntent().getStringExtra("fname");
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Class<Fragment> fClass = (Class<Fragment>) Class.forName(fname);
ft.add(fClass.newInstance(), ""); // $ hasTaintFlow
ft.add(0, Fragment.instantiate(this, fname), null); // $ hasTaintFlow
ft.add(0, Fragment.instantiate(this, fname, null)); // $ hasTaintFlow
ft.add(0, fClass, null, ""); // $ hasTaintFlow
ft.add(0, fClass.newInstance(), ""); // $ hasTaintFlow
ft.attach(fClass.newInstance()); // $ hasTaintFlow
ft.replace(0, fClass, null); // $ hasTaintFlow
ft.replace(0, fClass.newInstance()); // $ hasTaintFlow
ft.replace(0, fClass, null, ""); // $ hasTaintFlow
ft.replace(0, fClass.newInstance(), ""); // $ hasTaintFlow
ft.add(Fragment.class.newInstance(), ""); // Safe
ft.attach(Fragment.class.newInstance()); // Safe
ft.replace(0, Fragment.class.newInstance(), ""); // Safe
} catch (Exception e) {
}
}
}

View File

@@ -0,0 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/google-android-9.0.0