This skill provides guidelines and patterns to secure Android components (Activities, Services, Broadcast Receivers, Content Providers) and handle Intents safely, preventing privilege escalation and unauthorized access.
Glossary
- Intent: An asynchronous messaging object used to request an action from another app component.
- Exported component: A component (
android:exported="true") that can be launched by other apps on the device. - Sticky intent: A broadcast intent that remains in the system cache after it's sent, allowing any app to retrieve its contents.
- Signature permission: A permission whose protection level is set to
signature, granted only to apps signed with the same developer key. - onNewIntent: An activity lifecycle callback invoked when an activity is
launched with
FLAG_ACTIVITY_SINGLE_TOPand is already running at the top of the history stack. - PendingIntent: A token granted to a foreign application (for example, system services) allowing it to execute a predefined Intent with the creator's permissions.
- Mutable PendingIntent: A PendingIntent whose underlying Intent parameters can be modified by the receiving application.
- ContentProvider: A component that encapsulates data and provides it to other applications via standard query/insert interfaces.
- IntentSanitizer: A utility class in AndroidX Core used to build a safe, sanitized copy of an incoming Intent by filtering out unauthorized components, actions, or extras.
- Intent redirection (forwarding): A vulnerability where an application receives an intent from an untrusted source and uses it to launch a private, non-exported component.
Prerequisites
- The agent MUST be able to describe the function and security
implications of
onCreate,onNewIntent, and thesingleToplaunch mode. - The agent MUST be able to declare
<activity>,<service>,<receiver>, and<provider>tags inAndroidManifest.xmland define theirandroid:exportedandandroid:permissionattributes. - The agent MUST be able to implement signature verification checks using
PackageManager.
Limitations
- This skill focuses on local inter-component and inter-app communication security on the Android platform.
- This skill doesn't cover network security, web integration, or host-to-server security.
Setup and dependencies
- Android SDK: Minimum API Level 23 (Android 6.0) is required for standard hardware-backed keystore operations and component validation.
- AndroidX Core Library:
androidx.core:core:1.9.0or higher is mandatory to leverageIntentSanitizer. - Standard API access: Standard Android
PackageManagerAPIs are required for runtime component verification.
Intent security logic and decisions
1. Intent routing comparison
Evaluate the security features of different intent delivery methods:
| Intent Delivery Method | Scope | Recommended Use Case |
|---|---|---|
| Explicit Intent (Internal) | App Private | Launching internal activities/services |
| Implicit Intent | System Wide | Launching system camera, dialer, or sharing |
| Local Broadcasts (LocalBroadcastManager) (DEPRECATED) | App Private | Internal asynchronous event routing. Deprecated: Use in-app observers like Kotlin Flows/SharedFlow, LiveData, or reactive patterns instead. |
| System Broadcasts | System Wide | Receiving system events (NFC, Bluetooth) |
2. PendingIntent mutability flag options
Evaluate the security implications of PendingIntent mutability flags:
| Flag Name | Mutability | Recommended Use Case |
|---|---|---|
PendingIntent.FLAG_IMMUTABLE |
Immutable | Default for almost all PendingIntents, such as alarms and notifications |
PendingIntent.FLAG_MUTABLE |
Mutable | Inline notifications replies, slice actions (requires explicit target intent) |
3. Intent handling and redirection logic
IF (the component receives a nested Intent as an extra) {
IF (AndroidX Core 1.9.0+ and higher is available) {
MUST construct an IntentSanitizer to explicitly allowlist components, actions, data, and extras.
MUST call sanitizeByThrowing() or sanitizeByFiltering() before launching.
} ELSE {
MUST verify that the nested Intent's target package matches the current application package.
MUST verify that the target component of the nested Intent is publicly exported.
}
NEVER launch the nested Intent directly without validation.
} ELSE IF (the component handles broadcasts) {
MUST rely on the system's Protected Broadcast mechanism for system events (which guarantees the sender is the system framework).
MUST protect custom receivers with signature-level permissions or use RECEIVER_NOT_EXPORTED for dynamic receivers to restrict the sender.
}
4. PendingIntent security logic
IF (a PendingIntent is created for delivery to another application) {
MUST use PendingIntent.FLAG_IMMUTABLE by default.
IF (the PendingIntent must be mutable) {
MUST set the explicit target component or package name on the base Intent.
NEVER create an implicit, mutable PendingIntent.
}
}
5. ContentProvider security logic
IF (the ContentProvider is only for internal app use) {
MUST set android:exported="false".
} ELSE {
MUST protect it with android:readPermission and android:writePermission.
MUST set android:grantUriPermissions="false" unless temporary URL access is strictly required.
}
6. Service caller verification logic
IF (an exported service communicates with trusted sister/partner apps) {
MUST retrieve the calling UID using Binder.getCallingUid() and resolve it to package names using PackageManager.getPackagesForUid().
MUST verify that the calling package signature fingerprint matches your trusted certificate hash.
}
Code and configuration patterns
1. Safe intent redirection (manual verification)
Validate the target of a nested intent before launching it when modern sanitization libraries are unavailable.
- Expected Inputs:
- An incoming
Intentcontaining a nestedIntentextra namedEXTRA_NESTED_INTENT.
- An incoming
- Expected Outputs:
- Launches the target component if safe; throws
SecurityExceptionif validation fails.
- Launches the target component if safe; throws
fun safeIntentRedirectionManual() { val nestedIntent = IntentCompat.getParcelableExtra(intent, "EXTRA_NESTED_INTENT", Intent::class.java) if (nestedIntent != null) { // 1. Check for URI permission grants to prevent URI permission bypass val hasUriPermissionGrants = ( nestedIntent.flags and ( Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION ) ) != 0 if (hasUriPermissionGrants) { throw SecurityException("Nested intent contains forbidden URI permission grant flags!") } val pm = packageManager val target = nestedIntent.resolveActivity(pm) if (target != null) { // 2. Verify target is within the same package if (target.packageName != packageName) { throw SecurityException("Cross-app intent redirection is forbidden!") } try { // 3. Verify target activity is exported val info = pm.getActivityInfo(target, 0) if (!info.exported) { throw SecurityException("Target activity is private: ${target.className}") } // 4. Explicitly set the component to prevent intent interception nestedIntent.component = target // Safe to launch startActivity(nestedIntent) } catch (e: PackageManager.NameNotFoundException) { Log.e("Security", "Failed to resolve target activity", e) } } } }
2. Safe intent redirection using IntentSanitizer
Filter or reject dynamic intents using AndroidX IntentSanitizer (AndroidX Core
1.9.0+).
- Expected Inputs:
- An untrusted incoming
Intent.
- An untrusted incoming
- Expected Outputs:
Intent: A sanitized copy containing only allowlisted components, categories, and actions. ThrowsSecurityExceptionon violations if usingsanitizeByThrowing().
fun safeIntentRedirectionSanitizer() { val untrustedIntent = IntentCompat.getParcelableExtra(intent, "EXTRA_NESTED_INTENT", Intent::class.java) if (untrustedIntent != null) { // Define the strict boundaries for allowed redirection target val sanitizer = IntentSanitizer.Builder() .allowComponent(ComponentName("com.example.app", "com.example.app.SafeTargetActivity")) // Explicitly allowed target .allowAction(Intent.ACTION_VIEW) // Explicitly allowed actions .allowDataWithAuthority("com.example.app.provider") // Allowed URI authority .allowType("text/plain") // Allowed mime type .allowExtra("user_display_name", String::class.java) // Safe type-enforced extras // Note: URI permission flags are NOT allowed, so the sanitizer will automatically strip or throw on them .build() try { // Option A: Throws SecurityException if the intent violates policies val safeIntent = sanitizer.sanitizeByThrowing(untrustedIntent) startActivity(safeIntent) } catch (e: SecurityException) { Log.e("SECURITY_ALERT", "Attempted launch of non-allowlisted intent blocked", e) } // Option B: Silently filter and launch only the authorized parts (no exception thrown) // val filteredIntent = sanitizer.sanitizeByFiltering(untrustedIntent) // startActivity(filteredIntent) } }
3. Custom signature permission protection
Declare a custom signature-level permission in the manifest to secure family app communication.
- Expected Inputs: Manifest configuration.
- Expected Outputs: An activity that can only be launched by apps signed with the same developer certificate.
<permission android:name="com.example.snippets.permission.INTERNAL_COMMUNICATION" android:protectionLevel="signature" />
<activity android:name=".intents.InternalSharingActivity" android:exported="true" android:permission="com.example.snippets.permission.INTERNAL_COMMUNICATION"> <intent-filter> <action android:name="com.example.snippets.ACTION_SHARE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
4. Safe onNewIntent lifecycle verification (warm boot protection)
Ensure that activities reusing dynamic intents (for example, in background
launch paths) apply the same strict security filters inside onNewIntent.
- Expected Inputs:
newIntent(Intent): The newly delivered intent.
- Expected Outputs:
- Executes processing logic only if the new intent passes security validation.
override fun onNewIntent(newIntent: Intent) { super.onNewIntent(newIntent) // Set the intent to ensure intent returns the new one intent = newIntent // Validate the intent payload if (validateIntent(newIntent)) { processIntentPayload(newIntent) } else { Log.w("SECURITY_ALERT", "Received invalid or insecure intent during warm boot") } } private fun validateIntent(intent: Intent): Boolean { return intent.hasExtra("VALID_PAYLOAD_MARKER") }
5. Secure PendingIntent creation
Enforce immutability unless mutability is explicitly required.
- Expected Inputs (Immutable): An intent target.
- Expected Outputs (Immutable): A
PendingIntentthat cannot be altered by the receiver. - Expected Inputs (Mutable): An intent with an explicit component set.
- Expected Outputs (Mutable): A mutable
PendingIntentlocked to a specific receiver component to prevent hijacking.
fun createPendingIntents(context: Context) { // 1. Secure Immutable PendingIntent (Default) val intent = Intent(context, TargetActivity::class.java) val pendingIntent = PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) // 2. Secure Mutable PendingIntent (e.g., Notification Direct Reply) val mutableIntent = Intent().apply { // MUST set explicit target component to prevent redirection hijacking component = ComponentName(context, ReplyReceiver::class.java) } val mutablePendingIntent = PendingIntent.getBroadcast( context, 0, mutableIntent, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) }
6. Secure ContentProvider configuration and queries
Expose a ContentProvider securely and parameterize queries to prevent SQL injection.
- Expected Inputs:
uri(Uri): The query URI.projection(String[]): Columns to retrieve.selection(String): Query criteria.selectionArgs(String[]): Values mapping to selection placeholders (?).
- Expected Outputs:
Cursor: Filtered query results, strictly bound to projection maps.
<provider android:name=".intents.SecureDataProvider" android:authorities="com.example.snippets.provider" android:exported="true" android:readPermission="com.example.snippets.permission.READ_DATA" android:writePermission="com.example.snippets.permission.WRITE_DATA" android:grantUriPermissions="false" />
override fun query( uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String? ): Cursor? { val queryBuilder = SQLiteQueryBuilder() queryBuilder.tables = tableName // Strict projection map to prevent querying unauthorized columns queryBuilder.projectionMap = mapOf( "_id" to "_id", "display_name" to "display_name" ) // Enable strict validation (always available since minSdk is 36) queryBuilder.setStrict(true) queryBuilder.setStrictColumns(true) queryBuilder.setStrictGrammar(true) // MUST parameterize selection criteria; NEVER append selection strings directly val db = dbHelper.readableDatabase return queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder) }
7. Service caller signature verification
Verify the calling application's signature before binding to a service.
- Expected Inputs:
intent(Intent): The binding request intent.
- Expected Outputs:
IBinder: Local binder instance if caller signature matches trusted partner; throwsSecurityExceptionotherwise.
class SecureBoundService : Service() { companion object { // Expected SHA-256 hash of the trusted app's signing certificate (Base64 encoded) private const val TRUSTED_PARTNER_SHA256 = "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V=" } override fun onBind(intent: Intent): IBinder { // Return the binder. Do NOT perform signature verification in onBind() because // the binder connection is cached by Android, which can bypass checks on subsequent binds. return LocalBinder() } private fun enforceTrustedCaller() { val callingUid = Binder.getCallingUid() // Allow calls from the same application if (callingUid == Process.myUid()) { return } val pm = packageManager val packages = pm.getPackagesForUid(callingUid) if (packages.isNullOrEmpty() || !verifySignature(pm, packages[0])) { throw SecurityException("Access Denied: Caller signature is untrusted.") } } private fun verifySignature(pm: PackageManager, packageName: String): Boolean { try { val trustedSha256Raw = Base64.decode(TRUSTED_PARTNER_SHA256, Base64.DEFAULT) // API 28+ handles rotated certificates and avoids manual hashing. // Since minSdk is 36, this is always available. return pm.hasSigningCertificate(packageName, trustedSha256Raw, PackageManager.CERT_INPUT_SHA256) } catch (e: Exception) { Log.e("SECURITY_ERROR", "Verification failed for package: $packageName", e) } return false } inner class LocalBinder : Binder() { fun doSecureWork() { // Verify caller identity on every transaction method call enforceTrustedCaller() // Safe to proceed with sensitive operations } } }
Error handling
Handle component binding, database queries, and intent redirection failures securely to avoid exposing internal structures.
fun safeErrorHandling(callingPackage: String?) { try { val payload = intent.getStringExtra("DATA_EXTRA") ?: throw IllegalArgumentException("Payload parameter missing.") // Create a specific target intent using the validated payload val targetIntent = Intent(this, TargetActivity::class.java).apply { putExtra("SECURE_PAYLOAD", payload) } startActivity(targetIntent) } catch (e: SecurityException) { // MUST log security violations for audit, but NEVER expose exception details to the user. Log.e("SECURITY_ERROR", "Unauthorized component transition blocked. Calling Package: ${callingPackage ?: "Unknown"}", e) // MUST provide generic user feedback. showFeedbackToUser("Process request failed: Access Denied.") } catch (e: IllegalArgumentException) { Log.w("INTEGRITY_WARNING", "Missing intent parameter", e) } }
// Secure handling of ContentProvider queries on the client side:
try {
val cursor = contentResolver.query(providerUri, projection, selection, selectionArgs, null)
} catch (e: SQLiteException) {
Log.e("PROVIDER_ERROR", "ContentProvider database query failed", e)
// Secure handling: prevent raw query syntax details from leaking to UI
}
Reporting guidelines
When this skill is executed to apply security hardening updates to a codebase, the agent MUST generate a structured "Best Practices and Security Alignment Update" report for the developer. The report must be written to the session artifact folder (or printed in the final response) and include:
- Security alignment area: The category of improvement applied (for example, Safe Intent Redirection, Secure PendingIntent Configuration, ContentProvider Data Guarding).
- Impact and priority: The potential safety risk addressed by the update (for example, Component Hijacking Prevention, Private Data Isolation).
- Scope of changes: A list of all modified classes, XML files, and dependencies.
- Implementation summary: Concrete details of the solution (for example,
"Updated nested intent parsing to use the
IntentSanitizerAPI with a strict component allowlist"). - Code diff: Standard unified diffs showing the exact modifications.
Best practices and security alignment update template
Use the following markdown template when reporting changes to developers:
### Best practices and security alignment update: [Security Alignment Area]
* **Improvement Description:** [Brief description of the hardening update and why it's recommended]
* **Priority Level:** [High / Medium / Low]
* **Alignment Action:** [Summary of updates, for example, converted to FLAG_IMMUTABLE]
#### Files modified
* `[Relative path to File 1]`
* `[Relative path to File 2]`
#### Implementation diff
```diff
// Insert Unified Diff here
Testing and verification
- [Step 1 to verify the component behaves correctly, for example, run component unit test]
- [Step 2 to verify regression safety] ```
Antipatterns
- NEVER launch a nested
Intentreceived from an untrusted source without verifying its target package and exported status. - NEVER use sticky broadcasts (
sendStickyBroadcast). - NEVER assume an exported component is safe because it runs in a background thread or performs internal checks.
- NEVER expose sensitive functionalities (like SSO authentication or payment processors) to components without signature-level permission restrictions.
- NEVER process incoming intents in
onNewIntentwithout applying the same security controls asonCreate. - NEVER create a mutable
PendingIntentwithout setting an explicit target component in the baseIntent. - NEVER use dynamic string concatenation to construct selection blocks
inside a
ContentProviderquery. - NEVER use
Binder.getCallingUidinside aBroadcastReceiver.onReceiveto identify the sender of a broadcast, as it returns the receiver's own UID, not the sender's.
Best Practices
- MUST explicitly set
android:exported="false"for all components that don't need external communication. - MUST protect all exported components with custom permissions utilizing
android:protectionLevel="signature"when communicating between family apps. - MUST validate all incoming intent extras and handle missing parameters gracefully to prevent crashes.
- MUST rely on the system's Protected Broadcast mechanism for system events (for example, boot completed, package changes), as the system prevents untrusted apps from spoofing these actions.
- MUST protect custom broadcasts with signature-level permissions or use
RECEIVER_NOT_EXPORTEDfor dynamic receivers to restrict the sender identity. - MUST call
setIntent(newIntent)insideonNewIntent()before processing payloads to keep active references updated. - MUST use
PendingIntent.FLAG_IMMUTABLEby default when constructingPendingIntentinstances. - MUST protect exported
ContentProviderswithreadPermissionandwritePermission. - MUST enforce parameterized selection structures in
ContentProviderquery/update methods. - MUST verify the package signature fingerprint of binding applications at runtime inside exported services.
- MUST use
androidx.core.content.IntentSanitizerto sanitize incoming dynamic intents before redirection, if AndroidX Core 1.9.0+ is imported in the project.