Skip to content

CWE-926: Android Component Export

Overview

Android Component Export occurs when Android application components (activities, services, broadcast receivers, or content providers) are unintentionally exposed to other applications. This happens when components are not explicitly marked as exported or unexported, allowing unauthorized apps to interact with sensitive functionality.

OWASP Classification

A08:2025 - Software or Data Integrity Failures

Risk

High: Attackers can invoke exported components to bypass authentication, access sensitive data, trigger privileged operations, or cause denial of service. This can lead to data leakage, unauthorized actions, or application crashes.

Remediation Steps

Core principle: Never allow Android components to be accessible by other applications unless explicitly intended and protected; all activities, services, receivers, and providers must declare android:exported explicitly, and any component exposed beyond the application boundary must enforce authorization through signature-level permissions or runtime caller validation.

Identify all Android components requiring export control

  • Review the flaw details to identify which component lacks proper export declaration
  • Audit all activities, services, broadcast receivers, and content providers in AndroidManifest.xml
  • Identify components with intent filters (require explicit android:exported on Android 12+)
  • Determine which components should be internal-only vs. accessible to other apps
  • Check for components handling sensitive operations (admin panels, data providers, debugging services)

Set explicit export status for all components (Primary Defense)

  • Internal components: Set android:exported="false" for all activities, services, receivers, and providers that should only be accessed within your app
  • Public components: Set android:exported="true" only for components that must be accessible to other apps (launcher activities, public APIs, system receivers)
  • Android 12+ requirement: Components with intent filters MUST explicitly declare android:exported="true" or "false" - builds will fail without it
  • Default behavior varies by API level: Pre-Android 12, components with intent filters defaulted to exported - explicit declaration prevents security regressions

Protect exported components with signature-level permissions

  • Define custom permissions: Create permissions with android:protectionLevel="signature" for sensitive components
  • Assign permissions to components: Add android:permission attribute to all exported activities, services, receivers requiring authorization
  • Use signature protection: signature protection level ensures only apps signed with the same certificate can access the component
  • Separate read/write permissions: For content providers, use android:readPermission and android:writePermission to enforce different access levels
  • Avoid normal protection: Don't use normal or dangerous protection levels for sensitive components - they can be granted to any app

Implement runtime caller validation in component code

  • Validate caller UID: Use Binder.getCallingUid() in services to identify the calling app's UID
  • Verify caller package: Use getPackageManager().getPackagesForUid() to get package names, check against allowlist
  • Check caller signatures: Use PackageManager.GET_SIGNING_CERTIFICATES and verify SHA-256 fingerprints of caller's certificate match expected values
  • Fail closed: If caller validation fails, immediately finish activity or reject service binding - don't proceed with operation
  • Log unauthorized attempts: Record attempted unauthorized access for security monitoring

Validate all inputs from external callers

  • Don't trust intent extras: Even from verified callers, validate all data from intents (extras, data URIs, actions)
  • Use allowlists for actions: If component accepts action strings, validate against a strict allowlist of permitted actions
  • Sanitize paths and URIs: Never use caller-provided paths or URIs directly for file system or database operations
  • Bounds check numeric parameters: Validate array indices, counts, IDs are within expected ranges

Test component accessibility and permissions

  • Use ADB to test access: Use adb shell am start, am startservice, am broadcast to attempt launching components from external context
  • Verify permission denials: Confirm that internal components reject external access attempts with "Permission Denial" errors
  • Test with unauthorized apps: Install test apps without required permissions and verify they cannot access protected components
  • Unit test export status: Create tests that verify component exported attribute and required permissions via PackageManager
  • Enable lint checks: Use Android Lint to detect missing android:exported declarations and unprotected exported components
  • Run audit scripts: Use manifest parsing scripts to list all exported components and verify permission requirements

Vulnerable Patterns

Components Without Explicit Export Declaration

<!-- Activity implicitly exported due to intent filter (pre-Android 12) -->
<activity android:name=".AdminActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
    </intent-filter>
</activity>
<!-- On Android < 12: Any app can launch this activity! -->
<!-- On Android 12+: Build fails without explicit android:exported -->

<!-- Service without export declaration -->
<service android:name=".BackupService" />
<!-- Accessible externally if intent filter is present (pre-Android 12) -->

<!-- Broadcast receiver explicitly exported without protection -->
<receiver android:name=".DataReceiver"
    android:exported="true">
    <!-- Any app can send broadcasts to this receiver! -->
</receiver>

Components Exported Without Permission Protection

<!-- Activity exported without permission -->
<activity 
    android:name=".AdminActivity"
    android:exported="true">
    <!-- Any app can access admin functionality! -->
</activity>

<!-- Service exported without permission -->
<service 
    android:name=".APIService"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.API_ACTION" />
    </intent-filter>
    <!-- Any app can bind to this service! -->
</service>

<!-- Content provider exported without permissions -->
<provider
    android:name=".UserDataProvider"
    android:authorities="com.example.userdata"
    android:exported="true" />
<!-- Any app can read/write user data! -->

Missing Caller Validation in Component Code

// VULNERABLE - Activity accepts intents from any source
public class AdminActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // No validation of who launched this activity!
        String action = getIntent().getStringExtra("action");
        executeAdminAction(action);  // Dangerous!
    }

    private void executeAdminAction(String action) {
        // Performs privileged operations without verification
    }
}

// VULNERABLE - Service accepts any caller
public class APIService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return new APIBinder();
    }

    private class APIBinder extends Binder {
        public void performAction(String action) {
            // No caller validation!
            executeAction(action);
        }
    }
}

Secure Patterns

Explicitly Set Components as Not Exported

<!-- Activity marked as internal -->
<activity 
    android:name=".SettingsActivity"
    android:exported="false">
    <intent-filter>
        <action android:name="com.example.OPEN_SETTINGS" />
    </intent-filter>
</activity>
<!-- Only this app can access this activity -->

<!-- Internal services -->
<service 
    android:name=".DatabaseSyncService"
    android:exported="false" />

<service 
    android:name=".CacheCleanupService"
    android:exported="false" />

<!-- Internal broadcast receiver -->
<!-- Note: System broadcast delivery behavior can vary by API level.
     Ensure this configuration is intentional for your target SDK/device behavior. -->
<receiver
    android:name=".BootReceiver"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

<!-- Internal content providers -->
<provider
    android:name=".InternalDataProvider"
    android:authorities="com.example.internal"
    android:exported="false" />

Protect Exported Components with Permissions

<!-- Define custom permissions -->
<permission
    android:name="com.example.permission.ACCESS_API"
    android:protectionLevel="signature" />

<permission
    android:name="com.example.permission.ADMIN_ACCESS"
    android:protectionLevel="signature" />

<!-- Activity protected by signature permission -->
<activity 
    android:name=".AdminActivity"
    android:exported="true"
    android:permission="com.example.permission.ADMIN_ACCESS">
    <!-- Only apps signed with same key can access -->
</activity>

<!-- Service with permission protection -->
<service 
    android:name=".APIService"
    android:exported="true"
    android:permission="com.example.permission.ACCESS_API">
    <intent-filter>
        <action android:name="com.example.API_ACTION" />
    </intent-filter>
</service>

<!-- Receiver with system-level permission -->
<receiver 
    android:name=".SmsReceiver"
    android:exported="true"
    android:permission="android.permission.BROADCAST_SMS">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED" />
    </intent-filter>
</receiver>

<!-- Provider with separate read/write permissions -->
<provider
    android:name=".SharedDataProvider"
    android:authorities="com.example.shared"
    android:exported="true"
    android:readPermission="com.example.permission.READ_DATA"
    android:writePermission="com.example.permission.WRITE_DATA" />

Permission protection levels:

  • normal: Granted automatically (low risk)
  • dangerous: Requires user consent (privacy-sensitive)
  • signature: Only apps signed with same certificate (highest security)
  • signatureOrSystem: Legacy, avoid in new apps

Validate Caller Identity in Code

This example uses an any-match signer policy to tolerate key rotation.

AdminActivity
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.SigningInfo;
import android.os.Binder;
import android.os.Bundle;
import android.util.Log;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Validate Caller Identity in Code (defense-in-depth).
 *
 * IMPORTANT:
 * - Prefer manifest-level controls first (android:exported, android:permission with signature protection).
 * - Runtime caller validation is supplementary and should fail closed.
 */
public class AdminActivity extends Activity {
    private static final String TAG = "AdminActivity";

    /**
     * If you truly need to allow ONLY a specific external app, keep an allowlist.
     * Otherwise, prefer signature-permissions on the component.
     */
    private static final Set<String> ALLOWED_PACKAGES =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
                    "com.example.admin"
            )));

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // NOTE: getCallingPackage() is only reliable when started for result.
        // Treat this as defense-in-depth only.
        String callingPackage = getCallingPackage();
        if (callingPackage == null || !ALLOWED_PACKAGES.contains(callingPackage)) {
            Log.w(TAG, "Unauthorized access attempt (callingPackage): " + callingPackage);
            finish();
            return;
        }

        // Stronger check: verify the caller is signed by an expected signer.
        // Uses SHA-256 certificate fingerprints (order-insensitive).
        if (!isPackageSignedByAllowedSigner(callingPackage)) {
            Log.e(TAG, "Signature verification failed for: " + callingPackage);
            finish();
            return;
        }

        // Validate inputs even after caller verification (avoid intent injection).
        Intent intent = getIntent();
        String action = intent != null ? intent.getStringExtra("action") : null;
        if (action == null || action.isEmpty()) {
            Log.w(TAG, "Missing or empty action");
            finish();
            return;
        }

        executeAdminAction(action);
    }

    /**
     * Allowlist the signer(s) you trust. For example, the SHA-256 fingerprint of the admin app's signing cert.
     * Populate with your real fingerprints (uppercase/lowercase doesn't matter as we normalize).
     */
    private static final Set<String> ALLOWED_SIGNER_SHA256 =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
                    // Example placeholder (replace):
                    // "a1b2c3...deadbeef"
                    "REPLACE_WITH_REAL_SHA256_FINGERPRINT"
            )));

    private boolean isPackageSignedByAllowedSigner(String packageName) {
        try {
            PackageInfo pkgInfo = getPackageManager().getPackageInfo(
                    packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES
            );

            SigningInfo signingInfo = pkgInfo.signingInfo;
            if (signingInfo == null) return false;

            Signature[] signatures = signingInfo.hasMultipleSigners()
                    ? signingInfo.getApkContentsSigners()
                    : signingInfo.getSigningCertificateHistory();

            Set<String> signerFingerprints = signatureSetSha256(signatures);

            // Policy choice:
            // - Any-match: accept if ANY signer fingerprint is in our allowlist (good for rotation/lineage).
            // - Exact-match: require the whole set to equal a known set (stricter, can be brittle).
            for (String fp : signerFingerprints) {
                if (ALLOWED_SIGNER_SHA256.contains(fp)) return true;
            }
            return false;

        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Package not found: " + packageName, e);
            return false;
        } catch (NoSuchAlgorithmException e) {
            // Should not happen on Android; fail closed.
            Log.e(TAG, "SHA-256 unavailable", e);
            return false;
        }
    }

    private static Set<String> signatureSetSha256(Signature[] signatures) throws NoSuchAlgorithmException {
        if (signatures == null || signatures.length == 0) return Collections.emptySet();

        MessageDigest md = MessageDigest.getInstance("SHA-256");
        Set<String> out = new HashSet<>(signatures.length);

        for (Signature sig : signatures) {
            if (sig == null) continue;
            byte[] digest = md.digest(sig.toByteArray());
            out.add(toHexLower(digest));
            md.reset();
        }
        return out;
    }

    private static String toHexLower(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(Character.forDigit((b >> 4) & 0xF, 16));
            sb.append(Character.forDigit(b & 0xF, 16));
        }
        return sb.toString();
    }

    private void executeAdminAction(String action) {
        // Perform privileged operation (also validate/whitelist action values here).
    }
}
APIService
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.SigningInfo;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * SECURE - Service validates caller UID and signer (defense-in-depth).
 *
 * NOTE:
 * - For bound services, Binder.getCallingUid() is the right primitive.
 * - Always fail closed.
 */
public class APIService extends Service {
    private static final String TAG = "APIService";

    // Allowlist packages AND their signer fingerprints. Package name alone is not a trust boundary.
    private static final Set<String> AUTHORIZED_PACKAGES =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
                    "com.example.client1",
                    "com.example.client2"
            )));

    private static final Set<String> ALLOWED_CLIENT_SIGNER_SHA256 =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
                    // Example placeholder(s) (replace):
                    "REPLACE_WITH_REAL_SHA256_FINGERPRINT"
            )));

    @Override
    public IBinder onBind(Intent intent) {
        return new APIBinder();
    }

    private final class APIBinder extends Binder {
        public void performAction(String action) {
            // Basic input validation first.
            if (action == null || action.isEmpty()) {
                throw new IllegalArgumentException("action required");
            }

            int callingUid = Binder.getCallingUid();
            String[] pkgs = getPackageManager().getPackagesForUid(callingUid);

            if (pkgs == null || pkgs.length == 0) {
                Log.w(TAG, "Unable to determine caller for uid=" + callingUid);
                throw new SecurityException("Unable to determine caller");
            }

            // Authorize if ANY package for the UID is both allowlisted and signed by an allowed signer.
            for (String pkg : pkgs) {
                if (!AUTHORIZED_PACKAGES.contains(pkg)) continue;
                if (isPackageSignedByAllowedSigner(pkg, ALLOWED_CLIENT_SIGNER_SHA256)) {
                    executeAction(action);
                    return;
                }
            }

            Log.w(TAG, "Unauthorized caller uid=" + callingUid + " pkgs=" + Arrays.toString(pkgs));
            throw new SecurityException("Unauthorized caller");
        }
    }

    private boolean isPackageSignedByAllowedSigner(String packageName, Set<String> allowedSignerSha256) {
        try {
            PackageInfo pkgInfo = getPackageManager().getPackageInfo(
                    packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES
            );

            SigningInfo signingInfo = pkgInfo.signingInfo;
            if (signingInfo == null) return false;

            Signature[] signatures = signingInfo.hasMultipleSigners()
                    ? signingInfo.getApkContentsSigners()
                    : signingInfo.getSigningCertificateHistory();

            Set<String> fps = signatureSetSha256(signatures);

            // Any-match policy (recommended for rotation/lineage).
            for (String fp : fps) {
                if (allowedSignerSha256.contains(fp)) return true;
            }
            return false;

        } catch (PackageManager.NameNotFoundException e) {
            return false;
        } catch (NoSuchAlgorithmException e) {
            return false;
        }
    }

    private static Set<String> signatureSetSha256(Signature[] signatures) throws NoSuchAlgorithmException {
        if (signatures == null || signatures.length == 0) return Collections.emptySet();

        MessageDigest md = MessageDigest.getInstance("SHA-256");
        Set<String> out = new HashSet<>(signatures.length);

        for (Signature sig : signatures) {
            if (sig == null) continue;
            byte[] digest = md.digest(sig.toByteArray());
            out.add(toHexLower(digest));
            md.reset();
        }
        return out;
    }

    private static String toHexLower(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(Character.forDigit((b >> 4) & 0xF, 16));
            sb.append(Character.forDigit(b & 0xF, 16));
        }
        return sb.toString();
    }

    private void executeAction(String action) {
        // Perform API operation. Keep authorization checks separate from business logic.
    }
}

This is sometimes reported under CWE-926 by SAST tools.

Embedded DEX execution (android:useEmbeddedDex)

Some SAST scanners report the absence of android:useEmbeddedDex under CWE-926 (Android Component Export). While this setting does not affect component exporting, intent resolution, or IPC access control, it is sometimes grouped with CWE-926 due to its relationship to execution trust boundaries and application integrity.

On Android 10+ (API 29+), the android:useEmbeddedDex="true" attribute instructs the runtime to execute DEX code directly from the APK, rather than relying on locally compiled or optimized artifacts stored on the device.

Security relevance

  • Helps mitigate certain on-device code tampering scenarios, particularly on rooted devices where locally stored compiled artifacts could be modified.
  • Acts as defense-in-depth for application integrity.
  • Does not control which components are exported or accessible to other applications.

What this does not do:

  • Does not prevent reverse engineering (DEX is already present in the APK).
  • Does not restrict access to activities, services, receivers, or providers.
  • Does not replace manifest-level access controls (android:exported, permissions).

When to consider enabling:

  • High-value or security-sensitive applications
  • Environments where rooted devices are in scope
  • Apps subject to strict integrity or anti-tampering requirements

Example:

Note: This setting is independent of component export controls and should not be considered a primary mitigation for CWE-926. It is documented here to align with common SAST scanner findings and to highlight related integrity hardening measures.

Testing Your Fix

Audit Components with ADB

# List all exported components for your app
adb shell dumpsys package com.example.app | grep -A 5 "android:exported"

# Try to launch internal activity (should fail)
adb shell am start -n com.example.app/.InternalActivity
# Expected: Permission Denial

# Try to launch exported activity (should work)
adb shell am start -n com.example.app/.MainActivity

# Try to access internal service (should fail)
adb shell am startservice -n com.example.app/.InternalService
# Expected: Permission Denial

# Send broadcast to receiver
adb shell am broadcast -a com.example.ACTION -n com.example.app/.MyReceiver

# Query content provider
adb shell content query --uri content://com.example.internal/data
# Expected: Permission Denial for internal providers

Verification Steps

1. Manual AndroidManifest.xml review:

# Check all components have explicit android:exported declaration
grep -E "<activity|<service|<receiver|<provider" AndroidManifest.xml | grep -v "android:exported"
# Should return empty (all components declare exported status)

# Find exported components
grep 'android:exported="true"' AndroidManifest.xml
# Review each one - should have permission requirements

2. Verify exported components have permissions:

<!-- Check that each exported="true" component has android:permission -->
<!-- Example acceptable pattern: -->
<activity
    android:name=".AdminActivity"
    android:exported="true"
    android:permission="com.example.permission.ADMIN_ACCESS" />

<!-- Vulnerable pattern (no permission): -->
<activity
    android:name=".SettingsActivity"
    android:exported="true" />  <!-- MISSING android:permission! -->

3. Dynamic verification with ADB:

# Install app and test component access
adb install app.apk

# Try to start internal activity from another app
adb shell am start -n com.example/.InternalActivity
# Expected: Permission Denial or SecurityException

# Try to access content provider
adb shell content query --uri content://com.example/internal
# Expected: Permission Denial

# Check actual exported status at runtime
adb shell dumpsys package com.example | grep -A 5 "Activity\|Service\|Receiver"
# Verify exported status matches manifest

4. Automated static analysis:

# Use Android Lint to detect exported component issues
./gradlew lint

# Check for specific warnings:
# - ExportedReceiver: BroadcastReceiver is exported
# - ExportedService: Service is exported  
# - ExportedContentProvider: ContentProvider is exported
# - LaunchAnyWhere: Intent handling vulnerabilities

5. Review build output:

# After merging manifests, check final AndroidManifest.xml
cat app/build/intermediates/merged_manifests/*/AndroidManifest.xml | grep exported

Security verification checklist:

  • All components have explicit android:exported declaration
  • Components with android:exported="true" have android:permission set
  • Internal components use android:exported="false"
  • Custom permissions are properly defined and signature-protected
  • Intent filters on internal components are reviewed
  • Lint checks pass with no exported component warnings
  • ADB tests confirm components are not accessible without permission

Audit Script

#!/bin/bash
# audit-android-exports.sh - Audit AndroidManifest.xml for exported components

echo "Auditing exported Android components..."

MANIFEST="app/src/main/AndroidManifest.xml"

# Find components without explicit exported attribute
echo -e "\n=== Components missing explicit exported attribute ==="
grep -E '<(activity|service|receiver|provider)' "$MANIFEST" | \
    grep -v 'android:exported' | \
    grep -v '</'

# Find exported components
echo -e "\n=== Exported components ==="
grep -E 'android:exported="true"' "$MANIFEST"

# Find components with intent filters but no exported attribute
echo -e "\n=== Components with intent filters (verify export status) ==="
awk '/<(activity|service|receiver|provider)/,/<\/(activity|service|receiver|provider)>/' \
    "$MANIFEST" | \
    grep -B 5 '<intent-filter>' | \
    grep -E '<(activity|service|receiver|provider)'

# Check for exported components without permission protection
echo -e "\n=== Exported components without permission protection ==="
awk '/<(activity|service|receiver|provider)/,/<\/(activity|service|receiver|provider)>/' \
    "$MANIFEST" | \
    grep 'android:exported="true"' -A 3 | \
    grep -v 'android:permission'

# Check for useEmbeddedDex
echo -e "\n=== Application with embedded DEX enabled ==="
grep 'android:useEmbeddedDex="true"' "$MANIFEST"

echo -e "\nAudit complete"

Security Checklist

  • All activities have explicit android:exported attribute
  • All services have explicit android:exported attribute
  • All receivers have explicit android:exported attribute
  • All providers have explicit android:exported attribute
  • Internal components set exported="false"
  • android:useEmbeddedDex considered as integrity hardening
  • Exported components protected by custom permissions
  • Custom permissions use signature protection level for sensitive operations
  • Code validates caller identity for exported components
  • Lint warnings enabled for exported component checks
  • Tested that internal components cannot be accessed externally

Additional Resources