Mitigating and remediating intent-based Android security vulnerabilities

In previous posts we explored the potential for intent-based Android security vulnerabilities and then used Snyk Code to find exploits in popular apps on the Google Play store. If you know Snyk, you also know there’s no way we can just point out vulnerabilities and not recommend fixes.

Analyzing such an extensive dataset enabled us to review a lot of code. Through our research we’ve been exposed to various use cases that either significantly reduced or completely eliminated the attack vector. Although ours is not the official remediation advice from Google Play, they’re worth examining to gain further insight. And to be extra safe, we’ll also look at some remediation recommendations from Google Play.

Setting permissions for an activity

Users can set a permission for accessing an activity by specifying the android:permission attribute in the activity’s manifest declaration:

<activity android:theme="@style/theme" android:name="com.myapp.ProxyActivity" android:permission="com.myapp.PERMISSION">

In order to restrict the access only to components within the app, the permission tag can be defined with the android:protectionLevel=signature attribute. It is a permission granted only if the requesting app is signed with the same certificate as the app declaring it, thus disabling access from untrusted apps. 

We’ve seen this in one of the payment apps. It has the SecurityBlockerActivity which at first glance seems vulnerable to intent redirection. It takes the intent in last.intent parameter and loads it into startActivity():

public void onCreate(Bundle bundle) {
   String str;
   super.onCreate(bundle);
   setContentView(v.security_ui_activity_security_blocker);
   if (bundle != null) {
       this.last_intent = (Intent) bundle.getParcelable("last.intent");
		...
           this.type_blocker = dc(get_scenario_query_param());
           if (this.type_blocker == -1) {
               ec();
               return;
           }
       ...
   }

ec() starts the activity pointed to by this.last_intent while overriding its flags with a call to setFlags():

public void ec() {
   Intent intent = this.last_intent;
   if (intent != null) {
       startActivity(intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
   }
   overridePendingTransition(0, 0);
   finish();
}

But looking in the Android manifest file we observe the activity is declared with:

<activity android:theme="@style/Theme.MLTheme" android:name="com.paymentapp.android.security.security_ui.SecurityBlockerActivity" android:permission="com.paymentapp.wallet.permission.security.security_ui.ENTRY">

and:

<permission android:label="Permission for entry points of security_ui module" android:name="com.paymenyapp.wallet.permission.security.security_ui.ENTRY" android:protectionLevel="signature"/>

is protected by the signature level.

Setting flags on the redirected intent 

In some cases, an attacker wants to set flags on the redirected intent and route it back to their own app to gain some permissions. A common example is setting Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION, Intent.FLAG_GRANT_PREFIX_URI_PERMISSION, Intent.FLAG_GRANT_READ_URI_PERMISSION, or Intent.FLAG_GRANT_WRITE_URI_PERMISSION to allow access to a content or file provider. 

In order to prevent unintended flags being set, the removeFlags or setFlags methods can be used on the inner intent object. Note that just calling addFlags is not enough as it’ll add the requested flags to the existing ones. This remediation will prevent the attacker from getting access to providers but not from manipulating other properties that might get used in other sensitive parts of the program such as WebViews or business logic specific classes.

Avoiding use of URI_ALLOW_UNSAFE flag

As we’ve seen, intents can be converted to URI strings. Such strings start with the intent:// scheme and can be created from an intent object by calling intent.toUri(flags). Inversely, their value can be extracted from the extras bundle of another intent using getStringExtra and converted back to an intent object with the static method Intent.parseUri(uri, flags). It receives the string as the first argument and a flags integer as the second. By default this method will ignore Intent.FLAG_GRANT_* flags if they were set for the intent in the URI. But it is not the case if flags is set to URI_ALLOW_UNSAFE = 0x4

Avoiding use of URI_ALLOW_UNSAFE flag can help mitigate cases where an attacker wants to gain access to some sensitive providers requiring them to have read/write permissions.

Remediation recommendations from Google Play

In September 2019, the Google Play team issued remediation campaigns for various security vulnerabilities, including intent redirection. Developers whose apps were flagged as vulnerable were instructed to fix the issues according to Google’s recommendations. The official remediation advice for this vulnerability is detailed in their Remediation for Intent Redirection Vulnerability article. As vulnerable apps keep being discovered, it’s worth revisiting some of the prevention techniques for intent redirection vulnerabilities as they remain ever relevant.

Private vs. public can make all the difference

Your component needs to redirect Intents? That’s really fine. Does it also need to receive Intents from the outside world? Some components are left unnecessarily public, meaning their exported attribute equals to true in the AndroidManifest.xml file. Make sure to set this value to false if your component is only meant for internal use.

Validate the source of the intent

Before calling getIntent(), extracting the contents of the bundled data and invoking other activities to process it, a developer can check the origin of the intent using getCallingActivity:

 if (getCallingActivity().getPackageName().equals("desired_package")) {
   Intent intent = getIntent();
   Intent extra_intent = (Intent) intent.getParcelableExtra("extra_intent");
   startActivity(extra_intent);
 }

Note that checking if the calling package’s name starts with a string is risky as the attacker can control it by setting their app name to start with the prefix as seen in some of the cases.

Make sure the embedded intent is not harmful 

Intent redirection occurs when an activity can forward intents to arbitrary components allowing them to reach even unintended private and sensitive ones.  Usually, the redirecting activity is intended to launch only a single or a predefined set of activities. Limiting the activities or receivers that can be reached only to those meant by the developer can eliminate the problem. It can be done using a constructor that creates intents for a specific component by receiving a class argument:

public Intent (Context packageContext, Class<?> cls)

For example:

	Intent intent = new Intent(context, DesiredActivity.class);
	startActivity(intent);

This will limit the intent to be handled only by the DesiredActivity activity. Calling additional methods to set the intent’s parameters such as setFlags will further decrease the number of things an attacker can control.  

In addition, a developer can make sure the target component that should handle the intent is safe using resolveActivity:

 Intent intent = getIntent();
 Intent extra_intent = (Intent);
 intent.getParcelableExtra("extra_intent");
 if (extra_intent.resolveActivity(getPackageManager()).getPackageName().equals("desired_package") &&  extra_intent.resolveActivity(getPackageManager()).getClassName().equals("desired_class")) {
   startActivity(extra_intent);
 }

resolveActivity returns a non-null value only in case the activity was called with startActivityForResult. Making sure the return value is not null is insufficient and can easily be bypassed as witnessed in the shopping app.

As we’ve additionally seen in the social network app case, checking if the resolved component’s package name starts with a certain string can be tricked by setting the attacking app’s name to start with the same string.

Beware of deep links

From the Android documentation: “A deep link is an intent filter that allows users to directly enter a specific activity in your Android app”. They enable users to access activities in the app using a URL that matches the filters defined in the activity’s intent-filter. For instance:

activity android:name="com.myapp.ExampleActivity">
<intent-filter android:label="@string/filter_view_http_gizmos">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="myapp"
              android:host="myapp.com"
              android:pathPrefix="/home" />
    </intent-filter>
</activity>

This will allow reaching this activity by creating the following intent:

Intent intent = new Intent();
	intent.setAction("android.intent.action.VIEW");
	intent.setData(
Uri.parse("myapp://www.example.com/home?param_name=foo"));
	startActivity(intent);

It is also possible to reach this activity by simply navigating to myapp://www.example.com/home?param_name=foo in the browser.

As shown in the example the URL can carry query parameters which are usually handled with:

Uri uri = getIntent().getData();
String param = uri.getQueryParameter("param_name");

It is very important to properly sanitize such inputs, because, as we have seen recently in the TikTok application, they could be exploited by an attacker.

Showcasing a fix

After describing the vulnerabilities, analyzing some real app instances and detailing the recommended mitigation strategies, it’s a good place to showcase an app that did a great job in fixing the issue. We chose to focus on the rif is fun for Reddit app.

The vulnerability was disclosed to the team on March 8 and was fixed in version 4.19.9 that was uploaded to the Google Play store the next day on March 9.

Looking at the previously vulnerable activity we now see:

public void onPageFinished(WebView webView, String str) {
            super.onPageFinished(webView, str);
            Activity.this.k0(webView);
            this.a = true;
            String stringExtra = Activity.this.getIntent().getStringExtra("...");
            if (TextUtils.isEmpty(stringExtra)) {
                stringExtra = "";
            }
            String b2 = escapeEcmaScript(stringExtra); // !! THE FIX !!
            webView.loadUrl("javascript:..." + b2 + "\";})();");
        }

As mentioned, the vulnerability occurred due to a user-controlled parameter from the intent loaded into the WebView. The parameter represents the username and has no business being used without proper escaping. To fix this, the string was escaped with the escapeEcmaScript() method from StringEscapeUtils class in org.apache.commons.commons-text library. This is the recommended way to escape strings containing JavaScript code in Java. 

Conclusions

As one of the ecosystem’s core building blocks, Intents are a crucial part of an Android app’s inter-component and inter-app communication mechanisms. But as we’ve demonstrated, using them carelessly, especially when handling ones received from external apps, can cause severe security issues. As the responsibility of an application’s security continues to shift left, raising the awareness of mobile developers to such issues is key in mitigating them.

And it’s by no means an easy task. The richness of Android’s Java classes APIs creates multiple edge cases that a developer can easily get wrong as well as introducing incomplete fixes that leave components vulnerable. Using a peer review process, integrating a SAST tool — like Snyk Code — in the development cycle and adhering to the remediation advice brought forth in this post, must be set in place to make sure these hazards don’t occur.  

As Snyk Code continues to grow and evolve, our accumulated knowledge in this research created a good basis for improving our future Android support.

Get started with Snyk Code

Identify vulnerabilities in your code for free with Snyk Code.