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
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Application Data - Exported Content Providers or Activities may expose sensitive data to unauthorized applications. |
| Integrity | Scope: Integrity Modify Application Data - Malicious apps can modify data through exported Content Providers or corrupt state via exported Services. |
| Access Control | Scope: 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);
}
}
}
}
Related CWEs
- 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
- MITRE Corporation. "CWE-926: Improper Export of Android Application Components." https://cwe.mitre.org/data/definitions/926.html
- Android Developers. "App security best practices." https://developer.android.com/topic/security/best-practices
- OWASP. "Mobile Top 10." https://owasp.org/www-project-mobile-top-10/