Android Broadcast Receivers
What a Broadcast Receiver is (security perspective)
A Broadcast Receiver is an Android component designed to react to messages (âbroadcastsâ) sent by the system or by other apps. Security-wise, itâs an inbound message handler that can be invoked outside your appâs UI and often outside the userâs awareness. Many real-world receiver bugs arenât fancyâteams treat broadcasts as âinternal signalsâ and forget that a receiver can be an unauthenticated, parameter-driven entry point into privileged operations.
In assessments, receivers routinely show up as: silent state changes (toggle settings, start work, update config), implicit trust of extras, abuse of custom actions, or data leaks through ordered broadcasts or reply channels. The impact depends less on the receiver itself and more on what it triggers: services, jobs, file writes, account refresh, or exports.
How Receivers are reached
Receivers can be invoked via:
- Manifest receivers (
<receiver>): reachable based onexported, intent-filters, and permissions. - Dynamic receivers (
registerReceiver()): reachable only while registered; still vulnerable if exposed incorrectly. - System broadcasts: connectivity changes, package events, BOOT, SMS (restricted), etc.
- App-to-app broadcasts: explicit or implicit, normal or ordered, sometimes with a result channel.
The key point: a receiver can run without a UI, without a logged-in session initialized, and sometimes even when the app is not in the foreground. If it assumes âapp state existsâ, it becomes fragile and easy to abuse.
Web application comparison (clear mapping)
| Web | Android |
|---|---|
| Webhook endpoint | Exported receiver handling external broadcasts |
| Message queue consumer | Receiver triggered by system/app events |
| Unauthenticated POST with JSON body | Broadcast intent + extras (untrusted payload) |
| CSRF / forged request triggering state change | Forged broadcast triggering state change without verification |
| Request signature / shared secret | Receiver permission, signature-level permission, or explicit sender validation |
| Response data leakage | Ordered broadcast result / reply extras / exported side effects |
Treat an exported receiver like a webhook: if you allow untrusted senders, you must authenticate/authorize the message. If you donât need untrusted senders, donât expose it.
Why Broadcast Receivers become vulnerable
- Hidden entry point mindset: receivers are often written as âglue codeâ and missed in threat models and code reviews.
-
Custom actions without protection: developers add
com.app.SOMETHINGactions and assume only their app will send them. -
Weak/absent sender validation: relying on extras like
caller=trustedor not checking any identity at all. - Ordered broadcast misunderstandings: receivers can leak info via result extras or be influenced by other receivers in the chain.
- State changes without user context: a receiver triggers sensitive work (sync, logout, wipe cache, change environment) without confirming foreground context or user intent.
- Lifecycle constraints: receivers run with strict time limits and often in cold-start conditions, encouraging shortcuts and error-prone âjust start a serviceâ patterns.
Vulnerabilities you see in real assessments
-
Exported receiver allows unauthorized state change
Web equivalent: unauthenticated webhook that mutates state
Classic example: a receiver accepts
action=LOGOUT,action=REFRESH,action=SET_ENV, oraction=CLEAR_DATAand performs it without verifying sender or user context. -
Implicit broadcast interception / data leak via implicit intents
Web equivalent: sensitive data sent to the wrong callback URL
App broadcasts sensitive extras (OTP, auth state, identifiers) using an implicit intent. Any app that registers the same intent-filter can receive it.
-
Ordered broadcast result leakage
Web equivalent: leaking data in a response to an untrusted client
Receiver returns sensitive data through
setResultData/setResultExtrasor relies on ordered broadcast result channels without restricting who can initiate the chain. -
Permission misconfiguration (normal/dangerous instead of signature)
Web equivalent: âauthâ header that any client can mint
Receiver is protected by a permission that is not strong enough for the sensitivity of the action. In practice, itâs either missing entirely or uses a normal permission that third-party apps can request.
-
Dynamic receiver exposed too broadly
Web equivalent: temporarily exposing an admin route during a flow
A dynamic receiver is registered during onboarding/payment/etc. but uses a generic action string and no permission. Another app can race to send a forged broadcast while itâs registered.
-
DoS / crash via malformed extras
Web equivalent: crashing endpoint on invalid JSON
Receiver assumes required extras exist or parses them unsafely, allowing repeated crashes or repeated expensive work triggers.
How attackers exploit receivers
In the field, exploitation is usually straightforward: discover a reachable receiver, then send a broadcast that triggers privileged behavior. The subtle cases are (a) receivers that indirectly trigger a privileged service/job, and (b) apps that leak data by broadcasting it implicitly.
- Identify receivers from the manifest/dumpsys and classify exported + permissions + intent-filters.
- Trigger broadcasts using
adb am broadcast(explicit component or implicit action). - Tamper extras that control object IDs, modes, or routing (
env,accountId,debug,url). - Observe side effects: logs, shared prefs changes, network calls, started services/jobs, UI notifications.
- Abuse data flows: register a competing receiver to intercept implicit broadcasts or initiate ordered broadcasts to collect results.
Practical testing workflow (reproducible)
1) Enumerate receivers and protections
adb shell dumpsys package com.yourapp | sed -n '/Receivers:/,/Providers:/p'
Static manifest check (fast and reliable):
adb shell pm path com.yourapp
adb pull /data/app/<...>/base.apk
apktool d -f base.apk -o out
grep -R "<receiver" -n out/AndroidManifest.xml
What youâre looking for in each receiver:
android:exported(or default export behavior on older targets)android:permission(is it present? is it signature-level?)<intent-filter>actions (custom actions are high-signal)
2) Actively trigger suspected receivers
Explicit broadcast to a component:
adb shell am broadcast -n com.yourapp/.receiver.SyncReceiver
Broadcast with action + extras (typical tampering):
adb shell am broadcast -n com.yourapp/.receiver.CommandReceiver -a com.yourapp.ACTION_CMD --es cmd "logout"
adb shell am broadcast -n com.yourapp/.receiver.CommandReceiver -a com.yourapp.ACTION_CMD --es cmd "set_env" --es env "staging"
adb shell am broadcast -n com.yourapp/.receiver.RefreshReceiver -a com.yourapp.ACTION_REFRESH --es accountId "12345"
Implicit broadcast (tests interception risk and over-broad filters):
adb shell am broadcast -a com.yourapp.ACTION_REFRESH --es accountId "12345"
3) Observe what gets executed
Watch logs while triggering:
adb logcat | grep -i "yourapp\|Receiver\|Broadcast"
Check whether the receiver starts work (services/jobs):
adb shell dumpsys activity services | grep -i yourapp
adb shell dumpsys jobscheduler | grep -i yourapp
4) Validate interception / leak conditions
If you see your app sending broadcasts with sensitive extras, confirm whether they are explicit. A quick way is to observe
sendBroadcast calls dynamically in a lab build.
frida -U -f com.yourapp -l observe_broadcasts.js --no-pause
// observe_broadcasts.js (public-safe: logs action + component only)
Java.perform(function () {
var Ctx = Java.use("android.content.ContextWrapper");
Ctx.sendBroadcast.overload("android.content.Intent").implementation = function (i) {
try {
var a = i.getAction();
var c = i.getComponent();
console.log("[sendBroadcast] action=" + a + " component=" + (c ? c.flattenToShortString() : "null"));
} catch (e) {}
return this.sendBroadcast(i);
};
Ctx.sendOrderedBroadcast.overload("android.content.Intent", "java.lang.String").implementation = function (i, p) {
try {
var a = i.getAction();
var c = i.getComponent();
console.log("[sendOrderedBroadcast] action=" + a + " perm=" + p + " component=" + (c ? c.flattenToShortString() : "null"));
} catch (e) {}
return this.sendOrderedBroadcast(i, p);
};
});
5) Confirm permission enforcement
If the receiver is protected by a permission, try sending without it. On-device, you typically confirm by: (a) broadcast failing, (b) logs showing permission denial, or (c) no side effects.
adb shell am broadcast -n com.yourapp/.receiver.PrivReceiver -a com.yourapp.ACTION_PRIV
If you have a second test app, you can request the same permission and see whether it is grantable (normal/dangerous) versus signature-level (should fail unless same signing key).
Secure code (vulnerable â fixed) for each issue
1) Exported receiver allows unauthorized state change
Vulnerable (exported + command-like extras):
<receiver
android:name=".receiver.CommandReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.yourapp.ACTION_CMD"/>
</intent-filter>
</receiver>
// Kotlin
class CommandReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.getStringExtra("cmd")) {
"logout" -> SessionManager.logout(context)
"set_env" -> Config.setEnv(context, intent.getStringExtra("env") ?: "prod")
}
}
}
Fixed (reduce exposure + require strong permission):
<permission
android:name="com.yourapp.permission.INTERNAL_BROADCAST"
android:protectionLevel="signature" />
<receiver
android:name=".receiver.CommandReceiver"
android:exported="true"
android:permission="com.yourapp.permission.INTERNAL_BROADCAST">
<intent-filter>
<action android:name="com.yourapp.ACTION_CMD"/>
</intent-filter>
</receiver>
// Kotlin
class CommandReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Receiver is now gated by signature permission; keep logic minimal and defensive.
val cmd = intent.getStringExtra("cmd") ?: return
when (cmd) {
"logout" -> SessionManager.logout(context)
"set_env" -> {
val env = intent.getStringExtra("env") ?: return
if (env !in setOf("prod", "staging")) return
Config.setEnv(context, env)
}
}
}
}
2) Implicit broadcast interception (data leak)
Vulnerable (implicit intent with sensitive extras):
// Kotlin
val i = Intent("com.yourapp.ACTION_OTP")
i.putExtra("otp", otpValue)
sendBroadcast(i)
Fixed (explicit component or package + avoid putting secrets in broadcasts):
// Kotlin
val i = Intent("com.yourapp.ACTION_OTP")
i.setPackage(packageName) // limits delivery to your app
sendBroadcast(i)
// Kotlin (prefer: keep secret out of broadcast payload)
val i = Intent("com.yourapp.ACTION_OTP_READY")
i.setPackage(packageName)
sendBroadcast(i)
3) Ordered broadcast result leakage
Vulnerable:
// Kotlin
class TokenResultReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val token = SessionManager.getToken(context)
val b = Bundle()
b.putString("token", token)
setResultExtras(b)
}
}
Fixed (donât expose results to untrusted initiators; require signature permission):
<receiver
android:name=".receiver.TokenResultReceiver"
android:exported="true"
android:permission="com.yourapp.permission.INTERNAL_BROADCAST">
<intent-filter>
<action android:name="com.yourapp.ACTION_TOKEN_RESULT"/>
</intent-filter>
</receiver>
// Kotlin
class TokenResultReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Keep responses minimal; prefer returning non-sensitive flags.
val b = Bundle()
b.putBoolean("ok", SessionManager.isLoggedIn())
setResultExtras(b)
}
}
4) Permission misconfiguration
Vulnerable (normal permission, or no permission):
<receiver
android:name=".receiver.PrivReceiver"
android:exported="true" />
Fixed (signature-level permission for app-internal broadcasts):
<permission
android:name="com.yourapp.permission.INTERNAL_BROADCAST"
android:protectionLevel="signature" />
<receiver
android:name=".receiver.PrivReceiver"
android:exported="true"
android:permission="com.yourapp.permission.INTERNAL_BROADCAST" />
5) Dynamic receiver exposed too broadly
Vulnerable:
// Kotlin
val f = IntentFilter("com.yourapp.ACTION_STEP")
registerReceiver(stepReceiver, f)
Fixed (require a permission when registering):
// Kotlin
val f = IntentFilter("com.yourapp.ACTION_STEP")
registerReceiver(stepReceiver, f, "com.yourapp.permission.INTERNAL_BROADCAST", null)
6) DoS / crash via malformed extras
Vulnerable:
// Kotlin
override fun onReceive(context: Context, intent: Intent) {
val mode = intent.getStringExtra("mode")!! // crash if missing
Work.start(context, mode)
}
Fixed (validate + avoid expensive work on repeated triggers):
// Kotlin
override fun onReceive(context: Context, intent: Intent) {
val mode = intent.getStringExtra("mode") ?: return
if (mode !in setOf("light", "full")) return
Work.start(context, mode)
}
Interview questions & answers (Easy â Medium â Hard â Senior)
Easy
-
What is a Broadcast Receiver?
A component that reacts to broadcasts from the system or other apps. Itâs an inbound message handler that can run without a UI and can be a security boundary if exported.
-
What does
android:exportedmean for receivers?Whether other apps can send broadcasts to that receiver through intent resolution. If exported, treat the broadcast payload as attacker-controlled input.
-
Whatâs the web equivalent of an exported receiver?
A webhook endpoint. If it triggers state changes, it needs strong authentication/authorization or should not be exposed.
Medium
-
Whatâs a common real-world receiver vulnerability?
A custom-action receiver that performs sensitive work based on extras (command pattern) with no permission gate or weak sender assumptions.
-
Why are implicit broadcasts risky for confidentiality?
If the broadcast is implicit, any app can register an intent-filter and receive it. If you put sensitive data in extras, youâve created an unintended data distribution channel.
-
How do you quickly test a receiver?
Enumerate with manifest/dumpsys, then trigger with
adb shell am broadcast(explicit component and action). Watch side effects in logs, jobs, services, and app state.
Hard
-
When is a permission on a receiver not âgood enoughâ?
When the permission is normal/dangerous and third-party apps can request it. For truly internal broadcasts, you typically want a signature-level permission or a non-exported receiver, depending on the use case.
-
Whatâs the risk with ordered broadcasts?
Two main risks: (1) initiating an ordered broadcast to collect result data from receivers, and (2) other receivers in the chain influencing results. If you return sensitive data via result extras, you must restrict who can initiate or participate.
-
How do you validate interception risk during an assessment?
I look for app code that sends implicit broadcasts with sensitive extras and confirm delivery isnât restricted by component/package/permission. Then I verify whether any third-party receiver could register for that action (in real engagement, by analyzing intent-filters and runtime behavior).
Senior
-
How do you prioritize receiver findings in a large app?
I rank by: exported + custom action + privileged side effects. Next: any receiver that writes config/state, starts background work, touches account session, or participates in ordered broadcasts. I also flag any code broadcasting sensitive data implicitly.
-
Whatâs your âdefault secure designâ for internal app broadcasts?
Prefer explicit intents (component/package) and avoid carrying secrets in extras. If the message must be cross-process or potentially external, use a signature permission and keep the receiver behavior minimal, delegating authorization to the data/service layer.
-
What controls do you expect in code review for a receiver that must be exported?
A tight intent-filter, a strong permission gate, defensive parsing of extras, strict allowlists for modes/commands, no sensitive data returned through ordered results, and an explicit decision about foreground/user context if the broadcast triggers state changes.