Merge pull request #9983 from jcogs33/android-implicit-export

Java: query to detect implicitly exported Android components
This commit is contained in:
Jami
2022-08-24 10:52:50 -04:00
committed by GitHub
15 changed files with 533 additions and 1 deletions

View File

@@ -0,0 +1,8 @@
---
category: feature
---
* Added a new predicate, `requiresPermissions`, in the `AndroidComponentXmlElement` and `AndroidApplicationXmlElement` classes to detect if the element has explicitly set a value for its `android:permission` attribute.
* Added a new predicate, `hasAnIntentFilterElement`, in the `AndroidComponentXmlElement` class to detect if a component contains an intent filter element.
* Added a new predicate, `hasExportedAttribute`, in the `AndroidComponentXmlElement` class to detect if a component has an `android:exported` attribute.
* Added a new class, `AndroidCategoryXmlElement`, to represent a category element in an Android manifest file.
* Added a new predicate, `getACategoryElement`, in the `AndroidIntentFilterXmlElement` class to get a category element of an intent filter.

View File

@@ -0,0 +1,27 @@
/** Provides a class to identify implicitly exported Android components. */
private import semmle.code.xml.AndroidManifest
/**
* An Android component without an `exported` attribute explicitly set
* that also has an `intent-filter` element.
*/
class ImplicitlyExportedAndroidComponent extends AndroidComponentXmlElement {
ImplicitlyExportedAndroidComponent() {
this.hasAnIntentFilterElement() and
not this.hasExportedAttribute() and
// Components with category LAUNCHER or with action MAIN need to be exported since
// they are entry-points to the application. As a result, we do not consider these
// components to be implicitly exported since the developer intends them to be exported anyways.
not this.getAnIntentFilterElement().getACategoryElement().getCategoryName() =
"android.intent.category.LAUNCHER" and
not this.getAnIntentFilterElement().getAnActionElement().getActionName() =
"android.intent.action.MAIN" and
// The `permission` attribute can be used to limit components' exposure to other applications.
// As a result, we do not consider components with an explicitly set `permission` attribute to be
// implicitly exported since the developer has already limited access to such components.
not this.requiresPermissions() and
not this.getParent().(AndroidApplicationXmlElement).requiresPermissions() and
not this.getFile().(AndroidManifestXmlFile).isInBuildDirectory()
}
}

View File

@@ -67,6 +67,11 @@ class AndroidApplicationXmlElement extends XmlElement {
attr.getValue() = "true"
)
}
/**
* Holds if this application element has explicitly set a value for its `android:permission` attribute.
*/
predicate requiresPermissions() { this.getAnAttribute().(AndroidPermissionXmlAttribute).isFull() }
}
/**
@@ -108,7 +113,7 @@ class AndroidProviderXmlElement extends AndroidComponentXmlElement {
* `android:permission` attribute or its `android:readPermission` and `android:writePermission`
* attributes.
*/
predicate requiresPermissions() {
override predicate requiresPermissions() {
this.getAnAttribute().(AndroidPermissionXmlAttribute).isFull()
or
this.getAnAttribute().(AndroidPermissionXmlAttribute).isWrite() and
@@ -170,6 +175,11 @@ class AndroidComponentXmlElement extends XmlElement {
*/
AndroidIntentFilterXmlElement getAnIntentFilterElement() { result = this.getAChild() }
/**
* Holds if this component element has an `<intent-filter>` child element.
*/
predicate hasAnIntentFilterElement() { exists(this.getAnIntentFilterElement()) }
/**
* Gets the value of the `android:name` attribute of this component element.
*/
@@ -220,6 +230,16 @@ class AndroidComponentXmlElement extends XmlElement {
* Holds if the `android:exported` attribute of this component element is explicitly set to `false`.
*/
predicate isNotExported() { this.getExportedAttributeValue() = "false" }
/**
* Holds if this component element has an `android:exported` attribute.
*/
predicate hasExportedAttribute() { exists(this.getExportedAttributeValue()) }
/**
* Holds if this component element has explicitly set a value for its `android:permission` attribute.
*/
predicate requiresPermissions() { this.getAnAttribute().(AndroidPermissionXmlAttribute).isFull() }
}
/**
@@ -234,6 +254,11 @@ class AndroidIntentFilterXmlElement extends XmlElement {
* Gets an `<action>` child element of this `<intent-filter>` element.
*/
AndroidActionXmlElement getAnActionElement() { result = this.getAChild() }
/**
* Gets a `<category>` child element of this `<intent-filter>` element.
*/
AndroidCategoryXmlElement getACategoryElement() { result = this.getAChild() }
}
/**
@@ -257,3 +282,25 @@ class AndroidActionXmlElement extends XmlElement {
)
}
}
/**
* A `<category>` element in an Android manifest file.
*/
class AndroidCategoryXmlElement extends XmlElement {
AndroidCategoryXmlElement() {
this.getFile() instanceof AndroidManifestXmlFile and this.getName() = "category"
}
/**
* Gets the name of this category.
*/
string getCategoryName() {
exists(XmlAttribute attr |
attr = this.getAnAttribute() and
attr.getNamespace().getPrefix() = "android" and
attr.getName() = "name"
|
result = attr.getValue()
)
}
}

View File

@@ -0,0 +1,11 @@
<manifest ... >
<application ...
<!-- BAD: this component is implicitly exported -->
<activity>
android:name=".Activity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,12 @@
<manifest ... >
<application ...
<!-- GOOD: this component is not exported due to 'android:exported' explicitly set to 'false'-->
<activity>
android:name=".Activity">
android:exported="false"
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>The Android manifest file defines configuration settings for Android applications.
In this file, components can be declared with intent filters which specify what the components can do and what types
of intents the components can respond to. If the <code>android:exported</code> attribute is omitted from the component
when an intent filter is included, then the component will be implicitly exported.</p>
<p>An implicitly exported component could allow for improper access to the component and its data.</p>
</overview>
<recommendation>
<p>Explicitly set the <code>android:exported</code> attribute for every component or use permissions to limit access to the component.</p>
</recommendation>
<example>
<p>In the example below, the <code>android:exported</code> attribute is omitted when an intent filter is used.</p>
<sample src="ExampleBad.xml" />
<p>A corrected version sets the <code>android:exported</code> attribute to <code>false</code>.</p>
<sample src="ExampleGood.xml" />
</example>
<references>
<li>
Android Developers:
<a href="https://developer.android.com/guide/topics/manifest/manifest-intro">App Manifest Overview</a>.
</li>
<li>
Android Developers:
<a href="https://developer.android.com/guide/topics/manifest/intent-filter-element">The &lt;intent-filter&gt; element</a>.
</li>
<li>
Android Developers:
<a href="https://developer.android.com/guide/topics/manifest/activity-element#exported">The android:exported attribute</a>.
</li>
<li>
Android Developers:
<a href="https://developer.android.com/guide/topics/manifest/activity-element#prmsn">The android:permission attribute</a>.
</li>
<li>
Android Developers:
<a href="https://developer.android.com/about/versions/12/behavior-changes-12#exported">Safer component exporting</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,17 @@
/**
* @name Implicitly exported Android component
* @description Android components with an '<intent-filter>' and no 'android:exported' attribute are implicitly exported, which can allow for improper access to the components themselves and to their data.
* @kind problem
* @problem.severity warning
* @security-severity 8.2
* @id java/android/implicitly-exported-component
* @tags security
* external/cwe/cwe-926
* @precision high
*/
import java
import semmle.code.java.security.ImplicitlyExportedAndroidComponent
from ImplicitlyExportedAndroidComponent impExpAndroidComp
select impExpAndroidComp, "This component is implicitly exported."

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `java/android/implicitly-exported-component`, to detect if components are implicitly exported in the Android manifest.

View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.happybirthday">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HappyBirthday"
tools:targetApi="31">
<!-- $ hasImplicitExport --> <activity
android:name=".Activity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- $ hasImplicitExport --> <receiver
android:name=".CheckInstall">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_INSTALL"/>
</intent-filter>
</receiver>
<!-- $ hasImplicitExport --> <service
android:name=".backgroundService">
<intent-filter>
<action android:name="android.intent.action.START_BACKGROUND"/>
</intent-filter>
</service>
<!-- $ hasImplicitExport --> <provider
android:name=".MyCloudProvider">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: 'android:exported' explicitly set --> <activity
android:name=".Activity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: no intent filter --> <activity
android:name=".Activity">
</activity>
<!-- Safe: has 'permission' attribute --> <activity
android:name=".Activity"
android:permission=".Test">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: 'provider' with read and write permissions set --> <provider
android:name=".MyCloudProvider"
android:readPermission=".TestRead"
android:writePermission=".TestWrite">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- $ hasImplicitExport --> <provider
android:name=".MyCloudProvider"
android:readPermission=".TestRead">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- $ hasImplicitExport --> <provider
android:name=".MyCloudProvider"
android:writePermission=".TestWrite">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: has category 'android.intent.category.LAUNCHER' --> <activity
android:name=".Activity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Safe: has action 'android.intent.category.MAIN' --> <activity
android:name=".Activity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,18 @@
import java
import semmle.code.java.security.ImplicitlyExportedAndroidComponent
import TestUtilities.InlineExpectationsTest
class ImplicitlyExportedAndroidComponentTest extends InlineExpectationsTest {
ImplicitlyExportedAndroidComponentTest() { this = "ImplicitlyExportedAndroidComponentTest" }
override string getARelevantTag() { result = "hasImplicitExport" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasImplicitExport" and
exists(ImplicitlyExportedAndroidComponent impExpAndroidComp |
impExpAndroidComp.getLocation() = location and
element = impExpAndroidComp.toString() and
value = ""
)
}
}

View File

@@ -0,0 +1,3 @@
public class Test {
}

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.happybirthday">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HappyBirthday"
tools:targetApi="31"
android:permission=".Test">
<!-- Safe: 'application' element has 'permission' attribute --> <activity
android:name=".Activity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: 'application' element has 'permission' attribute --> <receiver
android:name=".CheckInstall">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_INSTALL"/>
</intent-filter>
</receiver>
<!-- Safe: 'application' element has 'permission' attribute --> <service
android:name=".backgroundService">
<intent-filter>
<action android:name="android.intent.action.START_BACKGROUND"/>
</intent-filter>
</service>
<!-- Safe: 'application' element has 'permission' attribute --> <provider
android:name=".MyCloudProvider">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: 'android:exported' explicitly set --> <activity
android:name=".Activity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: no intent filter --> <activity
android:name=".Activity">
</activity>
<!-- Safe: has 'permission' attribute --> <activity
android:name=".Activity"
android:permission=".Test">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: 'provider' with read and write permissions set --> <provider
android:name=".MyCloudProvider"
android:readPermission=".TestRead"
android:writePermission=".TestWrite">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: 'application' element has 'permission' attribute --> <provider
android:name=".MyCloudProvider"
android:readPermission=".TestRead">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: 'application' element has 'permission' attribute --> <provider
android:name=".MyCloudProvider"
android:writePermission=".TestWrite">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: has category 'android.intent.category.LAUNCHER' --> <activity
android:name=".Activity">
<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,107 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.happybirthday">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HappyBirthday"
tools:targetApi="31">
<!-- Safe: in build directory --> <activity
android:name=".Activity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: in build directory --> <receiver
android:name=".CheckInstall">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_INSTALL"/>
</intent-filter>
</receiver>
<!-- Safe: in build directory --> <service
android:name=".backgroundService">
<intent-filter>
<action android:name="android.intent.action.START_BACKGROUND"/>
</intent-filter>
</service>
<!-- Safe: in build directory --> <provider
android:name=".MyCloudProvider">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: 'android:exported' explicitly set --> <activity
android:name=".Activity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: no intent filter --> <activity
android:name=".Activity">
</activity>
<!-- Safe: has 'permission' attribute --> <activity
android:name=".Activity"
android:permission=".Test">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<!-- Safe: 'provider' with read and write permissions set --> <provider
android:name=".MyCloudProvider"
android:readPermission=".TestRead"
android:writePermission=".TestWrite">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: in build directory --> <provider
android:name=".MyCloudProvider"
android:readPermission=".TestRead">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: in build directory --> <provider
android:name=".MyCloudProvider"
android:writePermission=".TestWrite">
<intent-filter>
<action android:name="android.intent.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
<!-- Safe: has category 'android.intent.category.LAUNCHER' --> <activity
android:name=".Activity">
<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 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/google-android-9.0.0