Improper Verification of Intent by Broadcast Receiver

Description

Improper Verification of Intent by Broadcast Receiver is an Android-specific vulnerability where applications using Broadcast Receivers fail to verify that received Intents originate from authorized sources. While the Android operating system restricts certain implicit system intents (like ACTION_BOOT_COMPLETED) to be sent only by the OS itself, malicious applications can send explicit intents directly targeting a vulnerable application's Broadcast Receiver. Even receivers registered for implicit system intents will also receive explicit intents, potentially causing unintended behavior if the receiver doesn't validate the intent's source.

Risk

This vulnerability allows malicious applications to trigger actions in vulnerable applications by sending crafted intents. Attackers can cause denial of service by triggering shutdown or cleanup procedures. Sensitive operations intended only for system events can be triggered at will. Data may be exfiltrated if receivers process and respond to malicious intents. Application state can be corrupted by unexpected intent handling. In severe cases, attackers might trigger privileged operations that should only occur in response to legitimate system events.

Solution

Always verify the source of received intents in Broadcast Receivers. For receivers that should only respond to system intents, check that the intent is implicit (has no explicit component set). Use signature-level permissions to restrict who can send intents to your receivers. Consider using LocalBroadcastManager for internal application broadcasts. Set android:exported="false" for receivers that don't need to receive external intents. Validate all intent extras before processing. For sensitive operations, implement additional authorization checks beyond just receiving the intent.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Malicious applications can impersonate the operating system and trigger actions intended only for system events.
AvailabilityScope: Availability

DoS: Crash/Exit/Restart - Attackers can trigger shutdown or cleanup procedures, causing denial of service.
IntegrityScope: Integrity

Modify Application Data - Unexpected intent handling may corrupt application state or data.

Example Code

Vulnerable Code

// Vulnerable: BroadcastReceiver without intent verification
public class VulnerableShutdownReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

        // Vulnerable: No verification that this is from the system
        if (Intent.ACTION_SHUTDOWN.equals(action)) {
            // Attacker can trigger this with explicit intent!
            performShutdownCleanup(context);
            deleteTemporaryFiles(context);
            saveStateAndClose(context);
        }
    }

    private void performShutdownCleanup(Context context) {
        // Clear caches, save data, etc.
    }
}

// AndroidManifest.xml
// <receiver android:name=".VulnerableShutdownReceiver"
//           android:exported="true">
//     <intent-filter>
//         <action android:name="android.intent.action.ACTION_SHUTDOWN"/>
//     </intent-filter>
// </receiver>
// Attacker's malicious app
public class AttackerApp {

    public void triggerShutdown(Context context) {
        // Sending explicit intent to vulnerable receiver
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_SHUTDOWN);
        intent.setComponent(new ComponentName(
            "com.vulnerable.app",
            "com.vulnerable.app.VulnerableShutdownReceiver"
        ));
        context.sendBroadcast(intent);
        // Vulnerable app performs shutdown cleanup!
    }
}
// Vulnerable: Battery monitoring receiver
public class VulnerableBatteryReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

        // Vulnerable: Trusting battery level from any intent
        if (Intent.ACTION_BATTERY_LOW.equals(action)) {
            // Attacker can trigger low-battery mode
            enablePowerSavingMode(context);
            disableBackgroundSync(context);
            notifyUser("Battery low!");
        }

        if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
            // Attacker can disable power saving
            disablePowerSavingMode(context);
        }
    }
}
// Vulnerable: Package installation receiver
public class VulnerablePackageReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

        // Vulnerable: No source verification
        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
            Uri data = intent.getData();
            String packageName = data.getSchemeSpecificPart();

            // Attacker could trigger this with malicious package name
            logPackageInstallation(packageName);
            grantPermissionsToNewPackage(packageName);  // Dangerous!
        }
    }
}

Fixed Code

// Fixed: BroadcastReceiver with intent source verification
public class FixedShutdownReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // Fixed: Verify this is a system broadcast, not explicit intent
        if (!isSystemBroadcast(intent)) {
            Log.w(TAG, "Rejecting explicit intent attempting to trigger shutdown");
            return;
        }

        String action = intent.getAction();

        if (Intent.ACTION_SHUTDOWN.equals(action)) {
            performShutdownCleanup(context);
            deleteTemporaryFiles(context);
            saveStateAndClose(context);
        }
    }

    private boolean isSystemBroadcast(Intent intent) {
        // Fixed: Check if this is an implicit broadcast (no explicit target)
        // Explicit intents have component set
        return intent.getComponent() == null;
    }

    private void performShutdownCleanup(Context context) {
        // Safe to proceed
    }
}

// AndroidManifest.xml - Add permission requirement
// <receiver android:name=".FixedShutdownReceiver"
//           android:exported="true"
//           android:permission="android.permission.SHUTDOWN">
//     <intent-filter>
//         <action android:name="android.intent.action.ACTION_SHUTDOWN"/>
//     </intent-filter>
// </receiver>
// Fixed: Using signature permission for sensitive receivers
// In AndroidManifest.xml:
// <permission
//     android:name="com.myapp.permission.SENSITIVE_ACTION"
//     android:protectionLevel="signature"/>
//
// <receiver android:name=".SensitiveReceiver"
//           android:exported="true"
//           android:permission="com.myapp.permission.SENSITIVE_ACTION">

public class FixedSensitiveReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // Fixed: Even with explicit intent, only apps signed with
        // our key can have this permission

        // Additional verification for system intents
        if (isSystemAction(intent.getAction())) {
            if (!isFromSystem(context, intent)) {
                Log.w(TAG, "Ignoring non-system intent for system action");
                return;
            }
        }

        handleIntent(context, intent);
    }

    private boolean isSystemAction(String action) {
        return Intent.ACTION_SHUTDOWN.equals(action) ||
               Intent.ACTION_BOOT_COMPLETED.equals(action) ||
               Intent.ACTION_BATTERY_LOW.equals(action);
    }

    private boolean isFromSystem(Context context, Intent intent) {
        // Implicit broadcasts don't have component set
        return intent.getComponent() == null;
    }
}
// Fixed: Use LocalBroadcastManager for internal broadcasts
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

public class FixedInternalCommunication {

    private LocalBroadcastManager localBroadcastManager;
    private BroadcastReceiver receiver;

    public void setup(Context context) {
        // Fixed: Local broadcasts cannot be received by other apps
        localBroadcastManager = LocalBroadcastManager.getInstance(context);

        receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // Fixed: Only receives intents from within our app
                handleInternalMessage(intent);
            }
        };

        IntentFilter filter = new IntentFilter("com.myapp.INTERNAL_MESSAGE");
        localBroadcastManager.registerReceiver(receiver, filter);
    }

    public void sendInternalBroadcast(Intent intent) {
        // Fixed: This broadcast stays within the app
        localBroadcastManager.sendBroadcast(intent);
    }

    public void cleanup() {
        if (receiver != null) {
            localBroadcastManager.unregisterReceiver(receiver);
        }
    }
}
// Fixed: Comprehensive intent validation
public class FixedBatteryReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // Fixed: Multiple validation checks
        if (!validateIntent(context, intent)) {
            Log.w(TAG, "Invalid intent rejected");
            return;
        }

        String action = intent.getAction();

        if (Intent.ACTION_BATTERY_LOW.equals(action)) {
            // Additional check: verify battery actually is low
            IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            Intent batteryStatus = context.registerReceiver(null, filter);

            if (batteryStatus != null) {
                int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                float batteryPct = level * 100 / (float) scale;

                // Fixed: Only respond if battery is actually low
                if (batteryPct < 15) {
                    enablePowerSavingMode(context);
                }
            }
        }
    }

    private boolean validateIntent(Context context, Intent intent) {
        // Check 1: Must be implicit broadcast
        if (intent.getComponent() != null) {
            Log.w(TAG, "Rejecting explicit intent");
            return false;
        }

        // Check 2: Verify action is expected
        String action = intent.getAction();
        if (!Intent.ACTION_BATTERY_LOW.equals(action) &&
            !Intent.ACTION_BATTERY_OKAY.equals(action)) {
            return false;
        }

        // Check 3: Additional security checks as needed

        return true;
    }
}
// Fixed: Package receiver with proper validation
public class FixedPackageReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // Fixed: Verify system broadcast
        if (intent.getComponent() != null) {
            Log.w(TAG, "Rejecting explicit package intent");
            return;
        }

        String action = intent.getAction();

        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
            Uri data = intent.getData();
            if (data == null) {
                return;
            }

            String packageName = data.getSchemeSpecificPart();

            // Fixed: Validate package actually exists
            try {
                PackageManager pm = context.getPackageManager();
                PackageInfo info = pm.getPackageInfo(packageName, 0);

                // Fixed: Only log, don't grant permissions
                logPackageInstallation(packageName, info);

                // Granting permissions should require explicit user action
                // Not automatic based on broadcast
            } catch (PackageManager.NameNotFoundException e) {
                Log.w(TAG, "Package not found: " + packageName);
            }
        }
    }
}

// AndroidManifest.xml
// <receiver android:name=".FixedPackageReceiver"
//           android:exported="false">  <!-- Not exported if possible -->
//     <intent-filter>
//         <action android:name="android.intent.action.PACKAGE_ADDED"/>
//         <data android:scheme="package"/>
//     </intent-filter>
// </receiver>

  • CWE-940: Improper Verification of Source of a Communication Channel (parent)
  • CWE-926: Improper Export of Android Application Components (related)
  • CWE-927: Use of Implicit Intent for Sensitive Communication (related)

References

  1. MITRE Corporation. "CWE-925: Improper Verification of Intent by Broadcast Receiver." https://cwe.mitre.org/data/definitions/925.html
  2. Android Developers. "Broadcasts overview." https://developer.android.com/guide/components/broadcasts
  3. Android Developers. "Security tips." https://developer.android.com/training/articles/security-tips