Android Platform APIs
Android assigns a distinct system identity (Linux user ID and group ID) to every installed app. Because each Android app operates in a process sandbox, apps must explicitly request access to resources and data that are outside their sandbox. They request this access by declaring the permissions they need to use system data and features. Depending on how sensitive or critical the data or feature is, the Android system will grant the permission automatically or ask the user to approve the request.
Android permissions are classified into four different categories on the basis of the protection level they offer:
- Normal: This permission gives apps access to isolated application-level features with minimal risk to other apps, the user, and the system. For apps targeting Android 6.0 (API level 23) or higher, these permissions are granted automatically at installation time. For apps targeting a lower API level, the user needs to approve them at installation time. Example:
android.permission.INTERNET
. - Dangerous: This permission usually gives the app control over user data or control over the device in a way that impacts the user. This type of permission may not be granted at installation time; whether the app should have the permission may be left for the user to decide. Example:
android.permission.RECORD_AUDIO
. - Signature: This permission is granted only if the requesting app was signed with the same certificate used to sign the app that declared the permission. If the signature matches, the permission will be granted automatically. This permission is granted at installation time. Example:
android.permission.ACCESS_MOCK_LOCATION
. - SystemOrSignature: This permission is granted only to applications embedded in the system image or signed with the same certificate used to sign the application that declared the permission. Example:
android.permission.ACCESS_DOWNLOAD_MANAGER
.
A list of all permissions can be found in the Android developer documentation as well as concrete steps on how to:
The following changes affect all apps running on Android 8.0 (API level 26), even to those apps targeting lower API levels.
- Contacts provider usage stats change: when an app requests the
READ_CONTACTS
permission, queries for contact's usage data will return approximations rather than exact values (the auto-complete API is not affected by this change).
- Account access and discoverability improvements: Apps can no longer get access to user accounts only by having the
GET_ACCOUNTS
permission granted, unless the authenticator owns the accounts or the user grants that access. - New telephony permissions: the following permissions (classified as dangerous) are now part of the
PHONE
permissions group:- The
ANSWER_PHONE_CALLS
permission allows to answer incoming phone calls programmatically (viaacceptRingingCall
). - The
READ_PHONE_NUMBERS
permission grants read access to the phone numbers stored in the device.
- Restrictions when granting dangerous permissions: Dangerous permissions are classified into permission groups (e.g. the
STORAGE
group containsREAD_EXTERNAL_STORAGE
andWRITE_EXTERNAL_STORAGE
). Before Android 8.0 (API level 26), it was sufficient to request one permission of the group in order to get all permissions of that group also granted at the same time. This has changed starting at Android 8.0 (API level 26): whenever an app requests a permission at runtime, the system will grant exclusively that specific permission. However, note that all subsequent requests for permissions in that permission group will be automatically granted without showing the permissions dialog to the user. See this example from the Android developer documentation:Suppose an app lists both READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE in its manifest. The app requests READ_EXTERNAL_STORAGE and the user grants it. If the app targets API level 25 or lower, the system also grants WRITE_EXTERNAL_STORAGE at the same time, because it belongs to the same STORAGE permission group and is also registered in the manifest. If the app targets Android 8.0 (API level 26), the system grants only READ_EXTERNAL_STORAGE at that time; however, if the app later requests WRITE_EXTERNAL_STORAGE, the system immediately grants that privilege without prompting the user.You can see the list of permission groups in the Android developer documentation. To make this a bit more confusing, Google also warns that particular permissions might be moved from one group to another in future versions of the Android SDK and therefore, the logic of the app shouldn't rely on the structure of these permission groups. The best practice is to explicitly request every permission whenever it's needed.
The following changes affect all apps running on Android 9, even to those apps targeting API levels lower than 28.
- Restricted access to call logs:
READ_CALL_LOG
,WRITE_CALL_LOG
, andPROCESS_OUTGOING_CALLS
(dangerous) permissions are moved fromPHONE
to the newCALL_LOG
permission group. This means that being able to make phone calls (e.g. by having the permissions of thePHONE
group granted) is not sufficient to get access to the call logs. - Restricted access to phone numbers: apps wanting to read the phone number require the
READ_CALL_LOG
permission when running on Android 9 (API level 28). - Restricted access to Wi-Fi location and connection information: SSID and BSSID values cannot be retrieved (e.g. via
WifiManager.getConnectionInfo
unless all of the following is true:- The
ACCESS_FINE_LOCATION
orACCESS_COARSE_LOCATION
permission. - The
ACCESS_WIFI_STATE
permission. - Location services are enabled (under Settings -> Location).
- Build serial number deprecation: device's hardware serial number cannot be read (e.g. via
Build.getSerial
) unless theREAD_PHONE_STATE
(dangerous) permission is granted.
Android 10 Beta introduces several user privacy enhancements. The changes regarding permissions affect to all apps running on Android 10, including those targeting lower API levels.
- Restricted access to screen contents:
READ_FRAME_BUFFER
,CAPTURE_VIDEO_OUTPUT
, andCAPTURE_SECURE_VIDEO_OUTPUT
permissions are now signature-access only, which prevents silent access to the device's screen contents. - User-facing permission check on legacy apps: when running an app targeting Android 5.1 (API level 22) or lower for the first time, users will be prompted with a permissions screen where they can revoke access to specific legacy permissions (which previously would be automatically granted at installation time).
Permissions are applied via
android:permission
attribute within the <activity>
tag in the manifest. These permissions restrict which applications can start that Activity. The permission is checked during Context.startActivity
and Activity.startActivityForResult
. Not holding the required permission results in a SecurityException
being thrown from the call.Permissions applied via
android:permission
attribute within the <service>
tag in the manifest restrict who can start or bind to the associated Service. The permission is checked during Context.startService
, Context.stopService
and Context.bindService
. Not holding the required permission results in a SecurityException
being thrown from the call.Permissions applied via
android:permission
attribute within the <receiver>
tag restrict access to send broadcasts to the associated BroadcastReceiver
. The held permissions are checked after Context.sendBroadcast
returns, while trying to deliver the sent broadcast to the given receiver. Not holding the required permissions doesn't throw an exception, the result is an unsent broadcast.A permission can be supplied to
Context.registerReceiver
to control who can broadcast to a programmatically registered receiver. Going the other way, a permission can be supplied when calling Context.sendBroadcast
to restrict which broadcast receivers are allowed to receive the broadcast.Note that both a receiver and a broadcaster can require a permission. When this happens, both permission checks must pass for the intent to be delivered to the associated target. For more information, please reference the section "Restricting broadcasts with permissions" in the Android Developers Documentation.
Permissions applied via
android:permission
attribute within the <provider>
tag restrict access to data in a ContentProvider. Content providers have an important additional security facility called URI permissions which is described next. Unlike the other components, ContentProviders have two separate permission attributes that can be set, android:readPermission
restricts who can read from the provider, and android:writePermission
restricts who can write to it. If a ContentProvider is protected with both read and write permissions, holding only the write permission does not also grant read permissions.Permissions are checked when you first retrieve a provider and as operations are performed using the ContentProvider. Using
ContentResolver.query
requires holding the read permission; using ContentResolver.insert
, ContentResolver.update
, ContentResolver.delete
requires the write permission. A SecurityException
will be thrown from the call if proper permissions are not held in all these cases.The standard permission system is not sufficient when being used with content providers. For example a content provider may want to limit permissions to READ permissions in order to protect itself, while using custom URIs to retrieve information. An application should only have the permission for that specific URI.
The solution is per-URI permissions. When starting or returning a result from an activity, the method can set
Intent.FLAG_GRANT_READ_URI_PERMISSION
and/or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
. This grants permission to the activity for the specific URI regardless if it has permissions to access to data from the content provider.This allows a common capability-style model where user interaction drives ad-hoc granting of fine-grained permission. This can be a key facility for reducing the permissions needed by apps to only those directly related to their behavior. Without this model in place malicious users may access other member's email attachments or harvest contact lists for future use via unprotected URIs. In the manifest the
android:grantUriPermissions
attribute or the node help restrict the URIs.Android allows apps to expose their services/components to other apps. Custom permissions are required for app access to the exposed components. You can define custom permissions in
AndroidManifest.xml
by creating a permission tag with two mandatory attributes: android:name
and android:protectionLevel
.It is crucial to create custom permissions that adhere to the Principle of Least Privilege: permission should be defined explicitly for its purpose, with a meaningful and accurate label and description.
Below is an example of a custom permission called
START_MAIN_ACTIVITY
, which is required when launching the TEST_ACTIVITY
Activity.The first code block defines the new permission, which is self-explanatory. The label tag is a summary of the permission, and the description is a more detailed version of the summary. You can set the protection level according to the types of permissions that will be granted. Once you've defined your permission, you can enforce it by adding it to the application's manifest. In our example, the second block represents the component that we are going to restrict with the permission we created. It can be enforced by adding the
android:permission
attributes.<permission android:name="com.example.myapp.permission.START_MAIN_ACTIVITY"
android:label="Start Activity in myapp"
android:description="Allow the app to launch the activity of myapp app, any app you grant this permission will be able to launch main activity by myapp app."
android:protectionLevel="normal" />
​
<activity android:name="TEST_ACTIVITY"
android:permission="com.example.myapp.permission.START_MAIN_ACTIVITY">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Once the permission
START_MAIN_ACTIVITY
has been created, apps can request it via the uses-permission
tag in the AndroidManifest.xml
file. Any application granted the custom permission START_MAIN_ACTIVITY
can then launch the TEST_ACTIVITY
. Please note <uses-permission android:name="myapp.permission.START_MAIN_ACTIVITY" />
must be declared before the <application>
or an exception will occur at runtime. Please see the example below that is based on the permission overview and manifest-intro.<manifest>
<uses-permission android:name="com.example.myapp.permission.START_MAIN_ACTIVITY" />
<application>
<activity>
</activity>
</application>
</manifest>
We recommend using a reverse-domain annotation when registering a permission, as in the example above (e.g.
com.domain.application.permission
) in order to avoid collisions with other applications.Check permissions to make sure that the app really needs them and remove unnecessary permissions. For example, the
INTERNET
permission in the AndroidManifest.xml file is necessary for an Activity to load a web page into a WebView. Because a user can revoke an application's right to use a dangerous permission, the developer should check whether the application has the appropriate permission each time an action is performed that would require that permission.<uses-permission android:name="android.permission.INTERNET" />
Go through the permissions with the developer to identify the purpose of every permission set and remove unnecessary permissions.
Besides going through the AndroidManifest.xml file manually, you can also use the Android Asset Packaging tool (aapt) to examine the permissions of an APK file.
aapt comes with the Android SDK within the build-tools folder. It requires an APK file as input. You may list the APKs in the device by runningadb shell pm list packages -f | grep -i <keyword>
as seen in "Listing Installed Apps".
$ aapt d permissions app-x86-debug.apk
package: sg.vp.owasp_mobile.omtg_android
uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE'
uses-permission: name='android.permission.INTERNET'
Alternatively you may obtain a more detailed list of permissions via adb and the dumpsys tool:
$ adb shell dumpsys package sg.vp.owasp_mobile.omtg_android | grep permission
requested permissions:
android.permission.WRITE_EXTERNAL_STORAGE
android.permission.INTERNET
android.permission.READ_EXTERNAL_STORAGE
install permissions:
android.permission.INTERNET: granted=true
runtime permissions:
Please reference this permissions overview for descriptions of the listed permissions that are considered dangerous.
READ_CALENDAR
WRITE_CALENDAR
READ_CALL_LOG
WRITE_CALL_LOG
PROCESS_OUTGOING_CALLS
CAMERA
READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
RECORD_AUDIO
READ_PHONE_STATE
READ_PHONE_NUMBERS
CALL_PHONE
ANSWER_PHONE_CALLS
ADD_VOICEMAIL
USE_SIP
BODY_SENSORS
SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
Apart from enforcing custom permissions via the application manifest file, you can also check permissions programmatically. This is not recommended, however, because it is more error-prone and can be bypassed more easily with, e.g., runtime instrumentation. It is recommended that the
ContextCompat.checkSelfPermission
method is called to check if an activity has a specified permission. Whenever you see code like the following snippet, make sure that the same permissions are enforced in the manifest file.private static final String TAG = "LOG";
int canProcess = checkCallingOrSelfPermission("com.example.perm.READ_INCOMING_MSG");
if (canProcess != PERMISSION_GRANTED)
throw new SecurityException();
Or with
ContextCompat.checkSelfPermission
which compares it to the manifest file.if (ContextCompat.checkSelfPermission(secureActivity.this, Manifest.READ_INCOMING_MSG)
!= PackageManager.PERMISSION_GRANTED) {
//!= stands for not equals PERMISSION_GRANTED
Log.v(TAG, "Permission denied");
}
If your application has permissions that need to be requested at runtime, the application must call the
requestPermissions
method in order to obtain them. The app passes the permissions needed and an integer request code you have specified to the user asynchronously, returning once the user chooses to accept or deny the request in the same thread. After the response is returned the same request code is passed to the app's callback method.private static final String TAG = "LOG";
// We start by checking the permission of the current Activity
if (ContextCompat.checkSelfPermission(secureActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
​
// Permission is not granted
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(secureActivity.this,
//Gets whether you should show UI with rationale for requesting permission.
//You should do this only if you do not have permission and the permission requested rationale is not communicated clearly to the user.
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// Asynchronous thread waits for the users response.
// After the user sees the explanation try requesting the permission again.
} else {
// Request a permission that doesn't need to be explained.
ActivityCompat.requestPermissions(secureActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
// MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE will be the app-defined int constant.
// The callback method gets the result of the request.
}
} else {
// Permission already granted debug message printed in terminal.
Log.v(TAG, "Permission already granted.");
}
Please note that if you need to provide any information or explanation to the user it needs to be done before the call to
requestPermissions
, since the system dialog box can not be altered once called.Now your app has to override the system method
onRequestPermissionsResult
to see if the permission was granted. This method receives the requestCode
integer as input parameter (which is the same request code that was created in requestPermissions
).The following callback method may be used for
WRITE_EXTERNAL_STORAGE
.@Override //Needed to override system method onRequestPermissionsResult()
public void onRequestPermissionsResult(int requestCode, //requestCode is what you specified in requestPermissions()
String permissions[], int[] permissionResults) {
switch (requestCode) {
case MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE: {
if (grantResults.length > 0
&& permissionResults[0] == PackageManager.PERMISSION_GRANTED) {
// 0 is a canceled request, if int array equals requestCode permission is granted.
} else {
// permission denied code goes here.
Log.v(TAG, "Permission denied");
}
return;
}
// Other switch cases can be added here for multiple permission checks.
}
}
Permissions should be explicitly requested for every needed permission, even if a similar permission from the same group has already been requested. For applications targeting Android 7.1 (API level 25) and older, Android will automatically give an application all the permissions from a permission group, if the user grants one of the requested permissions of that group. Starting with Android 8.0 (API level 26), permissions will still automatically be granted if a user has already granted a permission from the same permission group, but the application still needs to explicitly request the permission. In this case, the
onRequestPermissionsResult
handler will automatically be triggered without any user interaction.For example if both
READ_EXTERNAL_STORAGE
and WRITE_EXTERNAL_STORAGE
are listed in the Android Manifest but only permissions are granted for READ_EXTERNAL_STORAGE
, then requesting WRITE_EXTERNAL_STORAGE
will automatically have permissions without user interaction because they are in the same group and not explicitly requested.Always check whether the application is requesting permissions it actually requires. Make sure that no permissions are requested which are not related to the goal of the app, especially
DANGEROUS
and SIGNATURE
permissions, since they can affect both the user and the application if mishandled. For instance, it should be suspicious if a single-player game app requires access to android.permission.WRITE_SMS
.When analyzing permissions, you should investigate the concrete use case scenarios of the app and always check if there are replacement APIs for any
DANGEROUS
permissions in use. A good example is the SMS Retriever API which streamlines the usage of SMS permissions when performing SMS-based user verification. By using this API an application does not have to declare DANGEROUS
permissions which is a benefit to both the user and developers of the application, who doesn't have to submit the Permissions Declaration Form.Permissions for installed applications can be retrieved with
adb
. The following extract demonstrates how to examine the permissions used by an application.$ adb shell dumpsys package com.google.android.youtube
...
declared permissions:
com.google.android.youtube.permission.C2D_MESSAGE: prot=signature, INSTALLED
requested permissions:
android.permission.INTERNET
android.permission.ACCESS_NETWORK_STATE
install permissions:
com.google.android.c2dm.permission.RECEIVE: granted=true
android.permission.USE_CREDENTIALS: granted=true
com.google.android.providers.gsf.permission.READ_GSERVICES: granted=true
...
The output shows all permissions using the following categories:
- declared permissions: list of all custom permissions.
- requested and install permissions: list of all install-time permissions including normal and signature permissions.
- runtime permissions: list of all dangerous permissions.
When doing the dynamic analysis: validate whether the permission requested by the app is actually necessary for the app. For instance: a single-player game that requires access to
android.permission.WRITE_SMS
, might not be a good idea.Android apps can expose functionality through custom URL schemes (which are a part of Intents). They can expose functionality to
- other apps (via IPC mechanisms, such as Intents, Binders, Android Shared Memory (ASHMEM), or BroadcastReceivers),
- the user (via the user interface).
None of the input from these sources can be trusted; it must be validated and/or sanitized. Validation ensures processing of data that the app is expecting only. If validation is not enforced, any input can be sent to the app, which may allow an attacker or malicious app to exploit app functionality.
The following portions of the source code should be checked if any app functionality has been exposed:
- Custom URL schemes. Check the test case "Testing Custom URL Schemes" as well for further test scenarios.
- IPC Mechanisms (Intents, Binders, Android Shared Memory, or BroadcastReceivers). Check the test case "Testing Whether Sensitive Data Is Exposed via IPC Mechanisms" as well for further test scenarios.
- User interface
An example of a vulnerable IPC mechanism is shown below.
You can use ContentProviders to access database information, and you can probe services to see if they return data. If data is not validated properly, the content provider may be prone to SQL injection while other apps are interacting with it. See the following vulnerable implementation of a ContentProvider.
<provider
android:name=".OMTG_CODING_003_SQL_Injection_Content_Provider_Implementation"
android:authorities="sg.vp.owasp_mobile.provider.College">
</provider>
The
AndroidManifest.xml
above defines a content provider that's exported and therefore available to all other apps. The query
function in the OMTG_CODING_003_SQL_Injection_Content_Provider_Implementation.java
class should be inspected.@Override
public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(STUDENTS_TABLE_NAME);
​
switch (uriMatcher.match(uri)) {
case STUDENTS:
qb.setProjectionMap(STUDENTS_PROJECTION_MAP);
break;
​
case STUDENT_ID:
// SQL Injection when providing an ID
qb.appendWhere( _ID + "=" + uri.getPathSegments().get(1));
Log.e("appendWhere",uri.getPathSegments().get(1).toString());
break;
​
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
​
if (sortOrder == null || sortOrder == ""){
/**
* By default sort on student names
*/
sortOrder = NAME;
}
Cursor c = qb.query(db, projection, selection, selectionArgs,null, null, sortOrder);
​
/**
* register to watch a content URI for changes
*/
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
While the user is providing a STUDENT_ID at
content://sg.vp.owasp_mobile.provider.College/students
, the query statement is prone to SQL injection. Obviously prepared statements must be used to avoid SQL injection, but input validation should also be applied so that only input that the app is expecting is processed.All app functions that process data coming in through the UI should implement input validation:
- For input from IPC or URL schemes, a validation function should be created. For example, the following determines whether the string is alphanumeric:
public boolean isAlphaNumeric(String s){
String pattern= "^[a-zA-Z0-9]*$";
return s.matches(pattern);
}
An alternative to validation functions is type conversion, with, for example,
Integer.parseInt
if only integers are expected. The OWASP Input Validation Cheat Sheet contains more information about this topic.The tester should manually test the input fields with strings like
OR 1=1--
if, for example, a local SQL injection vulnerability has been identified.On a rooted device, the command content can be used to query the data from a content provider. The following command queries the vulnerable function described above.
# content query --uri content://sg.vp.owasp_mobile.provider.College/students
SQL injection can be exploited with the following command. Instead of getting the record for Bob only, the user can retrieve all data.
# content query --uri content://sg.vp.owasp_mobile.provider.College/students --where "name='Bob') OR 1=1--''"
Android SDK offers developers a way to present a
Preferences activity
to users, allowing the developers to extend and adapt this abstract class.This abstract class parses the extra data fields of an Intent, in particular, the
PreferenceActivity.EXTRA_SHOW_FRAGMENT(:android:show_fragment)
and Preference Activity.EXTRA_SHOW_FRAGMENT_ARGUMENTS(:android:show_fragment_arguments)
fields.The first field is expected to contain the
Fragment
class name, and the second one is expected to contain the input bundle passed to the Fragment
.Because the
PreferenceActivity
uses reflection to load the fragment, an arbitrary class may be loaded inside the package or the Android SDK. The loaded class runs in the context of the application that exports this activity.With this vulnerability, an attacker can call fragments inside the target application or run the code present in other classes' constructors. Any class that's passed in the Intent and does not extend the Fragment class will cause a
java.lang.CastException
, but the empty constructor will be executed before the exception is thrown, allowing the code present in the class constructor run.To prevent this vulnerability, a new method called
isValidFragment
was added in Android 4.4 (API level 19). It allows developers to override this method and define the fragments that may be used in this context.The default implementation returns
true
on versions older than Android 4.4 (API level 19); it will throw an exception on later versions.Steps:
- Check if
android:targetSdkVersion
less than 19. - Find exported Activities that extend the
PreferenceActivity
class. - Determine whether the method
isValidFragment
has been overridden. - If the app currently sets its
android:targetSdkVersion
in the manifest to a value less than 19 and the vulnerable class does not contain any implementation ofisValidFragment
then, the vulnerability is inherited from thePreferenceActivity
. - In order to fix, developers should either update the
android:targetSdkVersion
to 19 or higher. Alternatively, if theandroid:targetSdkVersion
cannot be updated, then developers should implementisValidFragment
as described.
The following example shows an Activity that extends this activity:
public class MyPreferences extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
The following examples show the
isValidFragment
method being overridden with an implementation that allows the loading of MyPreferenceFragment
only:@Override
protected boolean isValidFragment(String fragmentName)
{
return "com.fullpackage.MyPreferenceFragment".equals(fragmentName);
}
MainActivity.class
public class MainActivity extends PreferenceActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
MyFragment.class
public class MyFragment extends Fragment {
public void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragmentLayout, null);
WebView myWebView = (WebView) wv.findViewById(R.id.webview);
myWebView.getSettings().setJavaScriptEnabled(true);
myWebView.loadUrl(this.getActivity().getIntent().getDataString());
return v;
}
}
To exploit this vulnerable Activity, you can create an application with the following code:
Intent i = new Intent();
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
i.setClassName("pt.claudio.insecurefragment","pt.claudio.insecurefragment.MainActivity");
i.putExtra(":android:show_fragment","pt.claudio.insecurefragment.MyFragment");
i.setData(Uri.parse("https://security.claudio.pt"));
startActivity(i);
WebViews are Android's embedded components which allow your app to open web pages within your application. In addition to mobile apps related threats, WebViews may expose your app to common web threats (e.g. XSS, Open Redirect, etc.).
One of the most important things to do when testing WebViews is to make sure that only trusted content can be loaded in it. Any newly loaded page could be potentially malicious, try to exploit any WebView bindings or try to phish the user. Unless you're developing a browser app, usually you'd like to restrict the pages being loaded to the domain of your app. A good practice is to prevent the user from even having the chance to input any URLs inside WebViews (which is the default on Android) nor navigate outside the trusted domains. Even when navigating on trusted domains there's still the risk that the user might encounter and click on other links to untrustworthy content (e.g. if the page allows for other users to post comments). In addition, some developers might even override some default behavior which can be potentially dangerous for the user. See the Static Analysis section below for more details.
To provide a safer web browsing experience, Android 8.1 (API level 27) introduces the
SafeBrowsing API
, which allows your application to detect URLs that Google has classified as a known threat.By default, WebViews show a warning to users about the security risk with the option to load the URL or stop the page from loading. With the SafeBrowsing API you can customize your application's behavior by either reporting the threat to SafeBrowsing or performing a particular action such as returning back to safety each time it encounters a known threat. Please check the Android Developers documentation for usage examples.
You can use the SafeBrowsing API independently from WebViews using the SafetyNet library, which implements a client for Safe Browsing Network Protocol v4. SafetyNet allows you to analyze all the URLs that your app is supposed load. You can check URLs with different schemes (e.g. http, file) since SafeBrowsing is agnostic to URL schemes, and against
TYPE_POTENTIALLY_HARMFUL_APPLICATION
and TYPE_SOCIAL_ENGINEERING
threat types.Virus Total provides an API for analyzing URLs and local files for known threats. The API Reference is available on Virus Total developers page.
When sending URLs or files to be checked for known threats make sure they don't contain sensitive data which could compromise a user's privacy, or expose sensitive content from your application.
As we mentioned before, handling page navigation should be analyzed carefully, especially when users might be able to navigate away from a trusted environment. The default and safest behavior on Android is to let the default web browser open any link that the user might click inside the WebView. However, this default logic can be modified by configuring a
WebViewClient
which allows navigation requests to be handled by the app itself. If this is the case, be sure to search for and inspect the following interception callback functions:shouldOverrideUrlLoading
allows your application to either abort loading WebViews with suspicious content by returningtrue
or allow the WebView to load the URL by returningfalse
. Considerations:- This method is not called for POST requests.
- This method is not called for XmlHttpRequests, iFrames, "src" attributes included in HTML or
<script>
tags. Instead,shouldInterceptRequest
should take care of this.
shouldInterceptRequest
allows the application to return the data from resource requests. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the data returned by theshouldInterceptRequest
method is used. Considerations:- This callback is invoked for a variety of URL schemes (e.g.,
http(s):
,data:
,file:
, etc.), not only those schemes which send requests over the network. - This is not called for
javascript:
orblob:
URLs, or for assets accessed viafile:///android_asset/
orfile:///android_res/
URLs. In the case of redirects, this is only called for the initial resource URL, not any subsequent redirect URLs. - When Safe Browsing is enabled, these URLs still undergo Safe Browsing checks but the developer can allow the URL with
setSafeBrowsingWhitelist
or even ignore the warning via theonSafeBrowsingHit
callback.
As you can see there are a lot of points to consider when testing the security of WebViews that have a WebViewClient configured, so be sure to carefully read and understand all of them by checking the
WebViewClient
Documentation.While the default value of
EnableSafeBrowsing
is true
, some applications might opt to disable it. To verify that SafeBrowsing is enabled, inspect the AndroidManifest.xml file and make sure that the configuration below is not present:<manifest>
<application>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
...
</application>
</manifest>
A convenient way to dynamically test deep linking is to use Frida or frida-trace and hook the
shouldOverrideUrlLoading
, shouldInterceptRequest
methods while using the app and clicking on links within the WebView. Be sure to also hook other related Uri
methods such as getHost
, getScheme
or getPath
which are typically used to inspect the requests and match known patterns or deny lists.Android allows you to create two different types of links for your apps: deep links and Android App Links. According to the Android Developer Documentation, deep links are URLs that take users directly to specific content in your app. You can set up deep links by adding intent filters and extracting data from incoming intents to drive users to the right activity. You can even use any custom scheme prefix such as
myapp
, which will result in the URI prefix "myapp://". These kind of deep links are also referred to as "Custom URL Schemes" and are typically used as a form of inter-app communication where an app can define certain actions (including the corresponding parameters) that can be triggered by other apps.This method of defining deep links via intent filters has an important issue: any other apps installed on a user's device can declare and try to handle the same intent (typically a custom URL scheme). This is known as deep link collision where any arbitrary application can declare control over the exact same URL custom scheme belonging to another application. In recent versions of Android this results in a so-called disambiguation dialog being shown to the user and asking them to select the application that should handle the link. The user could make the mistake of choosing a malicious application instead of the legitimate one.
Consider the following example of a deep link to an email application:
emailapp://composeEmail/[email protected]&message=SEND%20MONEY%20TO%20HERE!&sendImmediately=true
When a victim clicks such a link on a mobile device, a potentially vulnerable application might send an email from the user's original address containing attacker-crafted content. This could lead to financial loss, information disclosure, social damage of the victim, to name a few.
Another application specific example of deep linking is shown below:
myapp://mybeautifulapp/endpoint?Whatismyname=MyNameIs<svg onload=alert(1)>&MyAgeIs=100
This deep link could be used in order to abuse some known vulnerabilities already identified within an application (e.g. via reverse engineering). For instance, consider an application running a WebView with JavaScript enabled and rendering the
Whatismyname
parameter. In this concrete case, the deep link payload would trigger reflected cross site scripting within the context of the WebView.Since Android 6.0 (API Level 23) a developer can opt to define Android App Links, which are verified deep links based on a website URL explicitly registered by the developer. Clicking on an App Link will immediately open the app if it's installed and most importantly, the disambiguation dialog won't be prompted and therefore collisions are not possible anymore.
There are some key differences from regular deep links to consider:
- App Links only use
http://
andhttps://
schemes, any other custom URL schemes are not allowed. - App links do not suffer from deep link collision since they don't show a disambiguation dialog when a user opens them.
For every application, all existing deep links (including App Links) can potentially increase the app attack surface. All deep links must be enumerated and the actions they perform must be well tested, especially all input data which should be deemed to be untrustworthy and thus should be always validated. In addition, also consider the following:
- When using reflection-based persistence type of data processing, check the section "Testing Object Persistence" for Android.
- Using the data for queries? Make sure you make parameterized queries.
- Using the data to do authenticated actions? Make sure that the user is in an authenticated state before the data is processed.
- If tampering of the data will influence the result of the calculations: add an HMAC to the data.
You can easily determine whether deep links (with or without custom URL schemes) are defined just by inspecting the Android Manifest file and looking for
<intent-filter>
elements.The following example specifies a new deep link with a custom URL scheme called
myapp://
. You should pay special attention to the attributes as they give you clues about how the deep link is used. For example, the category BROWSABLE
will allow the deep link to be opened within a browser.<activity android:name=".MyUriActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="path" />
</intent-filter>
</activity>
The following example specifies a new App Link using both the
http://
and https://
schemes, along with the host and path which will activate it (in this case, the full URL would be https://www.myapp.com/my/app/path
):<activity android:name=".MyUriActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="www.myapp.com" android:path="/my/app/path" />
<data android:scheme="https" android:host="www.myapp.com" android:path="/my/app/path" />
</intent-filter>
</activity>
In this example, the
<intent-filter>
includes the flag android:autoVerify="true"
, which makes it an App Link and causes the Android system to reach out to the declared android:host
in an attempt to access the Digital Asset Links file in order to verify the App Links.You must pay special attention to deep links being used to transmit data (which is controlled externally, e.g. by the user or any other app). For example, the following URI could be used to transmit two values
valueOne
and valueTwo
: myapp://path/to/what/i/want?keyOne=valueOne&keyTwo=valueTwo
. In order to retrieve the input data and potentially process it, the receiving app could implement a code block similar to the following acting as a data handler method. The way to handle data is the same for both deep links and App Links:Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
String valueOne = uri.getQueryParameter("keyOne");
String valueTwo = uri.getQueryParameter("keyTwo");
}
The usage of the
getIntent
and getData
should be verified in order to understand how the application handles deep link input data, and if it could be subject to any kind of abuse. This general approach of locating these methods can be used across most applications when performing reverse engineering and is key when trying to understand how the application uses deep links and handles any externally provided input data.When testing deep links it's very useful to first build a list of all
<intent-filter>
elements from the AndroidManifest.xml and any custom URL schemes that they might define. For each of those deep links you should be able to determine which data they receive, if any. Remember that you might need to perform some reverse engineering first to find out if there are any input parameters that you might apply to the deep link. Sometimes you can even take advantage of other applications which you know that interact with your target app. You can reverse engineer them or use them as triggers, while hooking the data handler methods on the target app side. This way you can discover which ones are triggered and inspect valid or legitimate input parameters.Depending on the situation, the length of the link and the provided data you can use several methods call deep links. For very short deep links, probably the easiest method is to simply open your mobile browser and type it in the search bar. Another convenient method is to use the Activity Manager (am) tool to send intents within the Android device.
$ adb shell am start
-W -a android.intent.action.VIEW
-d "emailapp://composeEmail/[email protected]&message=SEND%20MONEY%20TO%20HERE!&sendImmediately=true" com.emailapp.android
$ adb shell am start
-W -a android.intent.action.VIEW
-d "https://www.myapp.com/my/app/path?dataparam=0" com.myapp.android
During implementation of a mobile application, developers may apply traditional techniques for IPC (such as using shared files or network sockets). The IPC system functionality offered by mobile application platforms should be used because it is much more mature than traditional techniques. Using IPC mechanisms with no security in mind may cause the application to leak or expose sensitive data.
The following is a list of Android IPC Mechanisms that may expose sensitive data:
We start by looking at the AndroidManifest.xml, where all activities, services, and content providers included in the source code must be declared (otherwise the system won't recognize them and they won't run). Broadcast receivers can be declared in the manifest or created dynamically. You will want to identify elements such as
An "exported" activity, service, or content can be accessed by other apps. There are two common ways to designate a component as exported. The obvious one is setting the export tag to true
android:exported="true"
. The second way involves defining an <intent-filter>
within the component element (<activity>
, <service>
, <receiver>
). When this is done, the export tag is automatically set to "true". To prevent all other Android apps from interacting with the IPC component element, be sure that the android:exported="true"
value and an <intent-filter>
aren't in their AndroidManifest.xml
files unless this is necessary.Remember that using the permission tag (
android:permission
) will also limit other applications' access to a component. If your IPC is intended to be accessible to other applications, you can apply a security policy with the <permission>
element and set a proper android:protectionLevel
. When android:permission
is used in a service declaration, other applications must declare a corresponding <uses-permission>
element in their own manifest to start, stop, or bind to the service.For more information about the content providers, please refer to the test case "Testing Whether Stored Sensitive Data Is Exposed via IPC Mechanisms" in chapter "Testing Data Storage".
Once you identify a list of IPC mechanisms, review the source code to see whether sensitive data is leaked when the mechanisms are used. For example, content providers can be used to access database information, and services can be probed to see if they return data. Broadcast receivers can leak sensitive information if probed or sniffed.
In the following, we use two example apps and give examples of identifying vulnerable IPC components:
In the "Sieve" app, we find three exported activities, identified by
<activity>
:<activity android:excludeFromRecents="true" android:label="@string/app_name" android:launchMode="singleTask" android:name=".MainLoginActivity" android:windowSoftInputMode="adjustResize|stateVisible">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:clearTaskOnLaunch="true" android:excludeFromRecents="true" android:exported="true" android:finishOnTaskLaunch="true" android:label="@string/title_activity_file_select" android:name=".FileSelectActivity" />
<activity android:clearTaskOnLaunch="true" android:excludeFromRecents="true" android:exported="true" android:finishOnTaskLaunch="true" android:label="@string/title_activity_pwlist" android:name=".PWList" />
By inspecting the
PWList.java
activity, we see that it offers options to list all keys, add, delete, etc. If we invoke it directly, we will be able to bypass the LoginActivity. More on this can be found in the dynamic analysis below.In the "Sieve" app, we find two exported services, identified by
<service>
:<service android:exported="true" android:name=".AuthService" android:process=":remote" />
<service android:exported="true" android:name=".CryptoService" android:process=":remote" />
Check the source code for the class
android.app.Service
:By reversing the target application, we can see that the service
AuthService
provides functionality for changing the password and PIN-protecting the target app. public void handleMessage(Message msg) {
AuthService.this.responseHandler = msg.replyTo;
Bundle returnBundle = msg.obj;
int responseCode;
int returnVal;
switch (msg.what) {
...
case AuthService.MSG_SET /*6345*/:
if (msg.arg1 == AuthService.TYPE_KEY) /*7452*/ {
responseCode = 42;
if (AuthService.this.setKey(returnBundle.getString("com.mwr.example.sieve.PASSWORD"))) {
returnVal = 0;
} else {
returnVal = 1;
}
} else if (msg.arg1 == AuthService.TYPE_PIN) {
responseCode = 41;
if (AuthService.this.setPin(returnBundle.getString("com.mwr.example.sieve.PIN"))) {
returnVal = 0;
} else {
returnVal = 1;
}
} else {
sendUnrecognisedMessage();
return;
}
}
}
In the "Android Insecure Bank" app, we find a broadcast receiver in the manifest, identified by
<receiver>
:<receiver android:exported="true" android:name="com.android.insecurebankv2.MyBroadCastReceiver">
<intent-filter>
<action android:name="theBroadcast" />
</intent-filter>
</receiver>