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
- Audit your existing intent filters. Open
AndroidManifest.xmland 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. - Set
android:exportedexplicitly. For every component with an intent filter, addandroid:exported="false"unless you specifically need other apps to start it. If you do need external access, set it totrueonly 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. - Use custom permissions for sensitive components. Define a custom permission in your manifest:
- Validate the sender in code. In your component’s
onCreate()(for activities) oronReceive()(for receivers), check the calling package: - Restrict data schemes and hosts. If your intent filter expects specific URIs, narrow them. For example, instead of
android:scheme="https", specifyandroid:host="api.yourdomain.com"and a specific path. This prevents other apps from using your component with arbitrary URLs. - Use
PendingIntentcarefully. When creating aPendingIntentthat 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 useFLAG_IMMUTABLE(orFLAG_MUTABLEonly when absolutely necessary). - For broadcast receivers, use
ContextCompat.registerReceiverwith flags. When registering dynamically, passContextCompat.RECEIVER_NOT_EXPORTEDto prevent other apps from sending broadcasts to it. For manifest-declared receivers, useandroid:exported="false"unless you need system broadcasts (like BOOT_COMPLETED) which require exported=true but should be protected with a signature permission. - 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. - Test with malicious intents. Use
adbto send crafted intents to your components: - 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:directBootAwareonly if needed, as it increases attack surface.
<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.
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.
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.
Common Mistakes
- Forgetting to set
android:exportedon pre-Android 12 devices – it defaults totrueif 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. UsesignatureorsignatureOrSystem. - Trusting the
EXTRA_ORIGINATING_UIDor 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_MUTABLEforPendingIntents 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:permissionon 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
- Fix Windows 10 Wi-Fi Not Connecting: Step-by-Step
- Scaling Headless Chrome: A Practical Guide
- Fix Windows Upgrade Error 0x80090011
- Fix Microsoft Store Apps Not Updating
- v0 Alternatives: Which Fits Your Web App Build?9.26 yd³
- Fix Hard Disk Error 0x80071ac3