Improper Export of Android Application Components

Description

Improper Export of Android Application Components occurs when an Android application exports Activities, Services, Content Providers, or Broadcast Receivers without properly restricting which applications can access them. By default, components with intent filters are exported (android:exported="true"), making them accessible to other applications on the device. When components handling sensitive operations are exported without proper access controls, malicious applications can launch these components to access sensitive data, trigger unauthorized actions, or corrupt application state.

Risk

Exported components without proper access controls create significant security risks. Exported Activities can expose sensitive user interfaces or skip authentication flows when launched directly. Exported Services can be started and bound to by malicious applications, allowing them to perform unauthorized operations or access internal functionality. Exported Content Providers (especially on Android versions before 4.2 where they're exported by default) can leak sensitive data or allow unauthorized modifications. Exported Broadcast Receivers can be triggered with malicious intents. The risk is compounded when these components handle sensitive data or perform privileged operations.

Solution

Explicitly set android:exported="false" for components that don't need to be accessed by other applications. For components that must be exported, use signature-level permissions to restrict access to applications signed with the same key. Implement proper authorization checks within exported components. Use intent validation to verify the source and contents of incoming intents. For Content Providers, use proper permissions with read/write distinctions. Consider using android:permission attributes to require callers to hold specific permissions. Audit all components in the manifest to ensure appropriate export settings.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Exported Content Providers or Activities may expose sensitive data to unauthorized applications.
IntegrityScope: Integrity

Modify Application Data - Malicious apps can modify data through exported Content Providers or corrupt state via exported Services.
Access ControlScope: Access Control

Bypass Protection Mechanism - Directly launching exported Activities may bypass authentication or authorization screens.

Example Code

Vulnerable Code

<!-- Vulnerable AndroidManifest.xml: Exported Activity -->
<activity android:name="com.example.app.AdminActivity">
    <!-- Vulnerable: Intent filter makes this exported by default -->
    <intent-filter>
        <action android:name="com.example.app.ADMIN_ACTION"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

<!-- Any app can start this admin activity! -->
<!-- Vulnerable: Exported Service without protection -->
<service android:name="com.example.app.DataSyncService"
         android:exported="true">
    <intent-filter>
        <action android:name="com.example.app.SYNC_DATA"/>
    </intent-filter>
</service>

<!-- Malicious apps can bind to this service -->
<!-- Vulnerable: Content Provider exported by default (pre-4.2) -->
<provider android:name="com.example.app.UserDataProvider"
          android:authorities="com.example.app.userdata">
    <!-- On Android < 4.2, this is exported by default! -->
</provider>
// Vulnerable: Exported Activity with sensitive data
public class VulnerableAdminActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.admin_layout);

        // Vulnerable: No verification that caller is authorized
        // Displays admin controls to any app that starts this activity
        displayAdminDashboard();
        showUserDatabase();
    }
}
// Vulnerable: Exported Service performing privileged operations
public class VulnerableDataService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = intent.getAction();

        // Vulnerable: Any app can trigger these actions
        if ("DELETE_ALL_DATA".equals(action)) {
            deleteAllUserData();  // Dangerous!
        } else if ("EXPORT_DATA".equals(action)) {
            exportSensitiveData();  // Data leak!
        }

        return START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Vulnerable: Returns binder to any app
        return new DataServiceBinder();
    }
}
// Malicious app exploiting exported components
public class MaliciousApp {

    public void exploitExportedActivity(Context context) {
        // Launching admin activity from malicious app
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(
            "com.example.vulnerableapp",
            "com.example.vulnerableapp.AdminActivity"
        ));
        context.startActivity(intent);
        // Gains access to admin UI!
    }

    public void exploitExportedService(Context context) {
        // Triggering dangerous service action
        Intent intent = new Intent("DELETE_ALL_DATA");
        intent.setPackage("com.example.vulnerableapp");
        context.startService(intent);
        // Deletes user data!
    }

    public void exploitExportedProvider(Context context) {
        // Reading data from exported provider
        Uri uri = Uri.parse("content://com.example.app.userdata/users");
        Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
        // Steals all user data!
    }
}

Fixed Code

<!-- Fixed AndroidManifest.xml: Protected components -->

<!-- Fixed: Explicitly not exported -->
<activity android:name="com.example.app.AdminActivity"
          android:exported="false">
    <!-- No intent filter needed for internal activities -->
</activity>

<!-- Fixed: Protected with signature permission -->
<permission android:name="com.example.app.permission.ADMIN_ACCESS"
            android:protectionLevel="signature"/>

<activity android:name="com.example.app.ExternalAdminActivity"
          android:exported="true"
          android:permission="com.example.app.permission.ADMIN_ACCESS">
    <intent-filter>
        <action android:name="com.example.app.ADMIN_ACTION"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>
<!-- Fixed: Service with proper protection -->
<service android:name="com.example.app.DataSyncService"
         android:exported="false">
    <!-- Internal service, not accessible to other apps -->
</service>

<!-- If service must be exported -->
<service android:name="com.example.app.ExternalService"
         android:exported="true"
         android:permission="com.example.app.permission.USE_SERVICE">
    <intent-filter>
        <action android:name="com.example.app.EXTERNAL_ACTION"/>
    </intent-filter>
</service>
<!-- Fixed: Content Provider with proper permissions -->
<provider android:name="com.example.app.UserDataProvider"
          android:authorities="com.example.app.userdata"
          android:exported="false">
    <!-- Not accessible to other apps -->
</provider>

<!-- If provider must be exported, use permissions -->
<provider android:name="com.example.app.PublicDataProvider"
          android:authorities="com.example.app.publicdata"
          android:exported="true"
          android:readPermission="com.example.app.permission.READ_DATA"
          android:writePermission="com.example.app.permission.WRITE_DATA">
</provider>
// Fixed: Activity with authorization checks
public class FixedAdminActivity extends Activity {

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

        // Fixed: Verify caller authorization even if exported
        if (!isCallerAuthorized()) {
            Log.w(TAG, "Unauthorized access attempt to admin activity");
            finish();
            return;
        }

        setContentView(R.layout.admin_layout);
        displayAdminDashboard();
    }

    private boolean isCallerAuthorized() {
        // Check if caller has required permission
        String callingPackage = getCallingPackage();
        if (callingPackage == null) {
            // Started internally
            return true;
        }

        // Verify caller has signature permission
        int permission = checkCallingPermission(
            "com.example.app.permission.ADMIN_ACCESS");
        return permission == PackageManager.PERMISSION_GRANTED;
    }
}
// Fixed: Service with action validation and authorization
public class FixedDataService extends Service {

    private static final Set<String> ALLOWED_ACTIONS = new HashSet<>(Arrays.asList(
        "SYNC_DATA",
        "CHECK_STATUS"
    ));

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Fixed: Validate caller
        if (!isCallerAuthorized()) {
            Log.w(TAG, "Unauthorized service start attempt");
            return START_NOT_STICKY;
        }

        String action = intent.getAction();

        // Fixed: Whitelist allowed actions
        if (!ALLOWED_ACTIONS.contains(action)) {
            Log.w(TAG, "Unknown action rejected: " + action);
            return START_NOT_STICKY;
        }

        if ("SYNC_DATA".equals(action)) {
            syncData();
        } else if ("CHECK_STATUS".equals(action)) {
            checkStatus();
        }

        return START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Fixed: Verify caller before returning binder
        if (!isCallerAuthorized()) {
            Log.w(TAG, "Unauthorized bind attempt");
            return null;
        }

        return new DataServiceBinder();
    }

    private boolean isCallerAuthorized() {
        int callingUid = Binder.getCallingUid();
        int myUid = Process.myUid();

        // Allow if same app
        if (callingUid == myUid) {
            return true;
        }

        // Check for required permission
        int permission = checkCallingPermission(
            "com.example.app.permission.USE_SERVICE");
        return permission == PackageManager.PERMISSION_GRANTED;
    }
}
// Fixed: Content Provider with proper access controls
public class FixedUserDataProvider extends ContentProvider {

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        // Fixed: Verify read permission
        if (getContext().checkCallingPermission(
                "com.example.app.permission.READ_DATA")
                != PackageManager.PERMISSION_GRANTED) {

            throw new SecurityException("Read permission required");
        }

        // Fixed: Validate and sanitize URI
        int match = uriMatcher.match(uri);
        switch (match) {
            case USERS:
                return queryUsers(projection, selection, selectionArgs, sortOrder);
            case USER_ID:
                return queryUser(uri.getLastPathSegment());
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // Fixed: Verify write permission
        if (getContext().checkCallingPermission(
                "com.example.app.permission.WRITE_DATA")
                != PackageManager.PERMISSION_GRANTED) {

            throw new SecurityException("Write permission required");
        }

        // Fixed: Validate input data
        validateContentValues(values);

        // Proceed with insert
        return doInsert(uri, values);
    }

    private void validateContentValues(ContentValues values) {
        // Ensure only allowed fields are set
        Set<String> allowedFields = new HashSet<>(Arrays.asList(
            "name", "email", "phone"
        ));

        for (String key : values.keySet()) {
            if (!allowedFields.contains(key)) {
                throw new IllegalArgumentException("Invalid field: " + key);
            }
        }
    }
}

  • CWE-285: Improper Authorization (parent)
  • CWE-925: Improper Verification of Intent by Broadcast Receiver (related)
  • CWE-927: Use of Implicit Intent for Sensitive Communication (related)

References

  1. MITRE Corporation. "CWE-926: Improper Export of Android Application Components." https://cwe.mitre.org/data/definitions/926.html
  2. Android Developers. "App security best practices." https://developer.android.com/topic/security/best-practices
  3. OWASP. "Mobile Top 10." https://owasp.org/www-project-mobile-top-10/