sodcalculator

10 Tech How-To

Secure Android Intent Filters: Step-by-Step Guide

Guide 10-38078 · Updated · Step by step

This guide shows you how to harden your Android app’s intent filters against spoofed or unexpected actions. By applying these steps, you’ll prevent malicious apps from triggering your components with forged intents, protecting user data and app integrity. You’ll need Android Studio and a basic understanding of Android manifests and Kotlin/Java.

What You’ll Need

  • Android Studio (latest stable version)
  • A project with at least one activity, service, or broadcast receiver that uses intent filters
  • Basic familiarity with AndroidManifest.xml and Kotlin or Java

Step-by-Step

  1. Audit your existing intent filters. Open AndroidManifest.xml and list every component (activity, service, receiver) that declares an <intent-filter>. For each, note the actions, categories, and data schemes. If a component doesn’t need to be launched by other apps, it shouldn’t have an intent filter at all.
  2. Set android:exported explicitly. For every component with an intent filter, add android:exported="false" unless you specifically need other apps to start it. If you do need external access, set it to true only after implementing the other protections below. On Android 12+ (API 31+), this attribute is required when an intent filter is present; omitting it causes a crash.
  3. Use custom permissions for sensitive components. Define a custom permission in your manifest:
  4. <permission android:name="com.example.app.ACCESS_PRIVATE" android:protectionLevel="signature" />

    Then require it on the component:

    <activity android:name=".SecretActivity" android:permission="com.example.app.ACCESS_PRIVATE" android:exported="true">

    This ensures only apps signed with the same certificate can trigger it.

  5. Validate the sender in code. In your component’s onCreate() (for activities) or onReceive() (for receivers), check the calling package:
  6. val caller = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        activity?.getCallingPackage()
    } else {
        @Suppress("DEPRECATION")
        activity?.getCallingActivity()?.packageName
    }

    Compare it against a whitelist of trusted packages. If it’s null or not in the list, finish the activity or ignore the broadcast.

  7. Restrict data schemes and hosts. If your intent filter expects specific URIs, narrow them. For example, instead of android:scheme="https", specify android:host="api.yourdomain.com" and a specific path. This prevents other apps from using your component with arbitrary URLs.
  8. Use PendingIntent carefully. When creating a PendingIntent that others can use, always specify an explicit component (e.g., Intent(context, MyReceiver::class.java)) rather than an implicit intent. Also set a unique request code and use FLAG_IMMUTABLE (or FLAG_MUTABLE only when absolutely necessary).
  9. For broadcast receivers, use ContextCompat.registerReceiver with flags. When registering dynamically, pass ContextCompat.RECEIVER_NOT_EXPORTED to prevent other apps from sending broadcasts to it. For manifest-declared receivers, use android:exported="false" unless you need system broadcasts (like BOOT_COMPLETED) which require exported=true but should be protected with a signature permission.
  10. Sanitize and validate all incoming intent extras. Treat any data from the intent as untrusted. Check types, ranges, and lengths before using them. For example, if you expect an integer extra, use getIntExtra("key", defaultValue) and validate the value.
  11. Test with malicious intents. Use adb to send crafted intents to your components:
  12. adb shell am start -n com.example.app/.SecretActivity --es "data" "malicious"

    Also try spoofing the package with --user 0 and different extras. Ensure your validation rejects them.

  13. Keep your exported components minimal. If a component doesn’t need to be public, remove its intent filter and export it only to specific apps via a custom permission. Consider using android:directBootAware only if needed, as it increases attack surface.

Common Mistakes

  • Forgetting to set android:exported on pre-Android 12 devices – it defaults to true if an intent filter is present, leaving your component open.
  • Using a custom permission with protectionLevel="normal" – any app can request it, so it doesn’t restrict access. Use signature or signatureOrSystem.
  • Trusting the EXTRA_ORIGINATING_UID or similar extras without verifying them – these can be spoofed.
  • Validating the calling package only in debug builds – release builds must have the same checks.
  • Using FLAG_MUTABLE for PendingIntents when not required, allowing malicious modifications.

Tips

  • Use Android’s security layers documentation as a reference.
  • For apps targeting Android 14 and above, the system enforces that exported components with intent filters must declare a permission. Set one even if it’s a custom permission.
  • Consider using android:permission on the <intent-filter> itself (available from API 33) to require a permission only for that specific filter.
  • Log all rejected intents with the calling package to help debug and detect attacks.
  • Use Android Profiler to monitor for unexpected component launches.

Troubleshooting

My app crashes with a SecurityException after adding a permission. Ensure the permission is declared in your manifest and that the calling app has it. If you’re testing with adb, you may need to grant the permission manually: adb shell pm grant com.example.app android.permission.YOUR_PERMISSION.

Other apps can’t start my component even with exported=true. Check that the intent filter’s action, category, and data match exactly what the caller sends. Also verify the permission is not blocking them.

getCallingPackage() returns null. This happens when the activity is started from a notification or a PendingIntent. In such cases, rely on the intent’s data and extras for validation, or require a permission.

My dynamic receiver stops working after adding RECEIVER_NOT_EXPORTED. System broadcasts (like BATTERY_LOW) cannot be received with this flag. Use RECEIVER_EXPORTED for system broadcasts, but validate the sender’s package in onReceive.

Android Studio warns about missing exported attribute. Add the attribute explicitly to every component with an intent filter, even if you set it to false.

Frequently Asked Questions

What is an intent filter spoofing attack?

An attacker sends an intent with a crafted action, data, or category to your component, tricking it into performing unintended actions, like revealing sensitive data or executing code.

Do I need to protect all intent filters?

Only those that are exported. If a component is not exported, only your app can start it, so it’s safe. But if it’s exported, you must protect it.

Can I use a signature permission to allow only my other apps?

Yes, if all your apps are signed with the same key, a signature permission will grant access only to those apps.

How do I test that my validation works?

Use adb to send intents with different package names (using --user or by creating a test app) and see if they are rejected. Also write unit tests that call your validation logic with malicious inputs.

Is it safe to use android:exported="true" for an activity that only shows a web page?

Not necessarily. If the activity loads URLs from the intent, an attacker could load a phishing page. Always validate the URL scheme and host, and consider using a custom permission.

What about android:autoVerify for app links?

App links verification helps, but it only works for HTTPS URLs. Still validate the host and path in your code, as verification can be bypassed on some devices.

Related

General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.

More in this bed

The whole Tech How-To bed →