Why Your Android App is Leaking Data: The PendingIntent Vulnerability You're Ignoring
How using FLAG_MUTABLE allows malicious apps to inject data into your Android app—and how to fix it.
If you are an Android developer, you’ve likely seen the lint warning: “Package-level requirement: specify FLAG_IMMUTABLE or FLAG_MUTABLE.” Since Android 12, Google has forced our hand. But many developers still choose FLAG_MUTABLE out of habit, unknowingly handing a "blank check" to potential attackers.
In this post, we’ll explore how a Mutable PendingIntent allows for unauthorized data injection and how to secure your app.
What is a PendingIntent anyway?
Think of a PendingIntent as a pre-authorized voucher. You are granting another application (like the System Notification Manager) the right to execute a piece of code as if it were your app, using your app’s permissions and identity.
The Vulnerability: The “Fill-in-the-Blanks” Problem
When you create a PendingIntent with FLAG_MUTABLE, you aren't just sending an instruction; you are sending an instruction with "empty slots."
While an attacker cannot reach into your app’s memory and rewrite the original Intent object, a mutable flag allows them to provide a “fill-in Intent” at the moment of execution. The Android system then merges the attacker’s data with your original Intent, potentially overriding extras or even changing the target component if not properly defined.
A Real-World Attack Scenario: The Discount Coupon Heist
Imagine a Shopping App that sends a notification when a user earns a 10% discount coupon.
The Vulnerable Code (Victim App)
The developer triggers a CouponReceiver when the user taps the notification but leaves the Intent mutable.
// The original intent intended for a 10% discount
val baseIntent = Intent(this, CouponReceiver::class.java).apply {
putExtra("discount_percent", 10)
}
// VULNERABLE: FLAG_MUTABLE allows other apps to inject "fill-in" data at send-time
val pendingIntent = PendingIntent.getBroadcast(
this, 0, baseIntent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)The Exploit (Attacker App)
A malicious app with NotificationListenerService permission (disguised as a system utility) waits for your notification. It cannot "edit" your Intent, but it can trigger it with its own data.
class MaliciousListener : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification) {
val pendingIntent = sbn.notification.contentIntent ?: return
// THE ATTACK: Creating a fill-in Intent with a 99% discount
val fillInIntent = Intent().apply {
putExtra("discount_percent", 99)
}
// Because the original was MUTABLE, Android merges the
// attacker-supplied fill-in Intent with your original one.
pendingIntent.send(this, 0, fillInIntent)
}
}The Result: Your app receives an Intent claiming a 99% discount. Because the Intent technically originated from your own PendingIntent, your logic trusts it.
The Golden Rule: FLAG_IMMUTABLE by Default
Since Android 12 (API 31), you must specify the mutability. To secure your app, you should almost always use FLAG_IMMUTABLE. This locks the Intent, preventing the system or any other app from merging fill-in data into it.
The Correct Way:
val pendingIntent = PendingIntent.getBroadcast(
this, 0, baseIntent,
PendingIntent.FLAG_IMMUTABLE // Secure: No data injection allowed!
)When should you use FLAG_MUTABLE? Only when the receiving app must add data, such as for Notification Direct Reply (where the system inserts the user's text) or certain Bubble intents.
Frequently Asked Questions (FAQs)
Does FLAG_UPDATE_CURRENT provide any security?
No. FLAG_UPDATE_CURRENT only manages how the system caches the PendingIntent. Security is strictly handled by the IMMUTABLE vs MUTABLE flags.
Can an attacker see the data inside an Immutable PendingIntent?
They can see notification metadata, but they cannot inject or override Intent fields (like “action” or “extras”) to redirect your app’s internal logic.
What about older Android versions (API < 23)?
FLAG_IMMUTABLE was only introduced in API 23. For older versions, security relies on explicit Intents (defining the exact class) and defensive coding—always validate that the incoming data is within an expected range.
Summary for the Security-Conscious Dev
- Default to
FLAG_IMMUTABLE. - Explicit Intents only: Always specify the component/class in your base Intent.
- Validate: Even with security flags, always sanity-check the data received in your
BroadcastReceiverorActivity.
Questions for the Community:
- Have you audited your
AlarmManagerorGeofencePendingIntents for mutability? - Do you use any specific lint rules or static analysis tools to catch insecure Intent flags?
- How are you handling the lack of
FLAG_IMMUTABLEon legacy devices (Pre-Marshmallow)?
📘 Master Your Next Technical Interview
Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.
- E-book (Best Value! 🚀): $1.99 on Google Play
- Kindle Edition: $3.49 on Amazon
- Also available in Paperback & Hardcover.

Comments
Post a Comment