đŸ›Ąïž Application Security CheatSheet

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:

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)

WebAndroid
Webhook endpointExported receiver handling external broadcasts
Message queue consumerReceiver triggered by system/app events
Unauthenticated POST with JSON bodyBroadcast intent + extras (untrusted payload)
CSRF / forged request triggering state changeForged broadcast triggering state change without verification
Request signature / shared secretReceiver permission, signature-level permission, or explicit sender validation
Response data leakageOrdered 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

  1. Hidden entry point mindset: receivers are often written as “glue code” and missed in threat models and code reviews.
  2. Custom actions without protection: developers add com.app.SOMETHING actions and assume only their app will send them.
  3. Weak/absent sender validation: relying on extras like caller=trusted or not checking any identity at all.
  4. Ordered broadcast misunderstandings: receivers can leak info via result extras or be influenced by other receivers in the chain.
  5. State changes without user context: a receiver triggers sensitive work (sync, logout, wipe cache, change environment) without confirming foreground context or user intent.
  6. 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

  1. 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, or action=CLEAR_DATA and performs it without verifying sender or user context.

  2. 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.

  3. Ordered broadcast result leakage
    Web equivalent: leaking data in a response to an untrusted client

    Receiver returns sensitive data through setResultData/setResultExtras or relies on ordered broadcast result channels without restricting who can initiate the chain.

  4. 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.

  5. 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.

  6. 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.

  1. Identify receivers from the manifest/dumpsys and classify exported + permissions + intent-filters.
  2. Trigger broadcasts using adb am broadcast (explicit component or implicit action).
  3. Tamper extras that control object IDs, modes, or routing (env, accountId, debug, url).
  4. Observe side effects: logs, shared prefs changes, network calls, started services/jobs, UI notifications.
  5. 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:

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

  1. 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.

  2. What does android:exported mean for receivers?

    Whether other apps can send broadcasts to that receiver through intent resolution. If exported, treat the broadcast payload as attacker-controlled input.

  3. 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

  1. 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.

  2. 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.

  3. 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

  1. 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.

  2. 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.

  3. 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

  1. 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.

  2. 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.

  3. 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.