Android Intent Security
Organizationandroid
Audits, detects, and remediates Android Intent and component security vulnerabilities. Use this skill when auditing component configurations in AndroidManifest.xml (activities, services, receivers) or source code handling incoming Intents (getIntent, getParcelableExtra) to prevent Intent Redirection and unauthorized access. Don't use for network security, TLS configuration, or general runtime permission request flows.
Overview
- Repository
- android/skills
- Publisher
- android
- Stars
- 10.5K
- Forks
- 695
- Bundled files
- 0
- License
- Apache-2.0
- Updated
- Nov 11, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/android/skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/security/android-intent-security" ~/.claude/skills/android-intent-security
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/android/skills.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/security/android-intent-security" ~/.codex/skills/android-intent-security
rm -rf "$tmp"Install the Android Intent Security agent skill from https://github.com/android/skills/tree/main/security/android-intent-security into this project's skills folder.Instructions
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") accessible to 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 that allows the receiving application to modify its underlying Intent parameters.
- ContentProvider: A component that encapsulates data and provides it to other applications using standard query and 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 with an explicit allowlist of
components, actions, data, and extras.
MUST call sanitizeByThrowing() or sanitizeByFiltering() before
launching.
} ELSE {
MUST reject or strip URI permission grant flags
(FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION,
FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
FLAG_GRANT_PREFIX_URI_PERMISSION).
MUST resolve the target component using resolveActivity() and verify
that both its packageName and className are on an explicit
allowlist.
}
NEVER launch the nested Intent directly without validation.
}
IF (the component handles broadcasts) {
MUST rely on the system's Protected Broadcast mechanism for system events
(which verifies that 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.
IF (using ContextCompat.registerReceiver with RECEIVER_NOT_EXPORTED AND
minSdk < 33) {
MUST verify that the
${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
<permission> (with android:protectionLevel="signature") and
<uses-permission> elements are declared in AndroidManifest.xml.
}
}
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 or partner apps) {
MUST expose a cross-process interface (such as an AIDL Stub or
Messenger) instead of a local Binder subclass.
MUST read the caller UID on every IPC request using
Binder.getCallingUid() in AIDL or Message.sendingUid in Messenger.
MUST reject callingUid == Process.myUid() so calls outside an active IPC
transaction fail closed.
MUST verify that the calling package signing certificate matches the
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!")
}
// 2. Clear any selector intent that could override resolution
nestedIntent.selector = null
// 3. Resolve the target activity and verify both packageName and className
// match the expected allowlisted destination
val target = nestedIntent.resolveActivity(packageManager)
if (target != null &&
target.packageName == packageName &&
target.className == TargetActivity::class.java.name
) {
// 4. Explicitly pin the resolved component to prevent intent interception
nestedIntent.component = target
startActivity(nestedIntent)
} else {
throw SecurityException("Invalid or disallowed redirect target: $target")
}
}
}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 components, categories, and actions on the allowlist. ThrowsSecurityExceptionon violations if usingsanitizeByThrowing().
fun safeIntentRedirectionSanitizer() {
// Allowlist: anything NOT listed is rejected (only component is required; other fields are optional).
// No allowFlags(): every flag, including FLAG_GRANT_*_URI_PERMISSION, is rejected.
val sanitizer = IntentSanitizer.Builder()
.allowComponent(ComponentName(this, TargetActivity::class.java))
.allowAction(Intent.ACTION_VIEW)
.allowData { it.scheme == ContentResolver.SCHEME_CONTENT && it.authority == "$packageName.provider" }
.allowType("text/plain")
.allowExtra("user_display_name", String::class.java) { it.length <= 100 } // type and content
.build()
val safeIntent = try {
val untrusted = IntentCompat.getParcelableExtra(intent, "EXTRA_NESTED_INTENT", Intent::class.java)
?: return
sanitizer.sanitizeByThrowing(untrusted)
} catch (_: SecurityException) {
Log.w("SECURITY_ALERT", "Blocked non-allowlisted nested intent")
return
} catch (_: BadParcelableException) { // e.g. an extra naming an unknown Parcelable/Serializable
Log.w("SECURITY_ALERT", "Malformed nested intent")
return
}
// The sanitizer doesn't enforce presence; check what the target actually requires.
if (safeIntent.getStringExtra("user_display_name") == null) {
Log.w("SECURITY_ALERT", "Nested intent missing user_display_name")
return
}
startActivity(safeIntent)
}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 restricted to 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 signing certificate on incoming cross-process IPC calls in an exported bound service.
- Expected Inputs:
- An incoming cross-process Binder transaction (for example, an AIDL
Stubmethod call oronTransact()).
- An incoming cross-process Binder transaction (for example, an AIDL
- Expected Outputs:
- Executes the requested service operation if the calling UID's signing certificate matches the trusted partner hash; throws
SecurityExceptionotherwise.
- Executes the requested service operation if the calling UID's signing certificate matches the trusted partner hash; throws
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="
}
// Use an AIDL Stub for cross-process IPC
private val binder = object : IMyService.Stub() {
override fun doSecureWork() {
// Verify caller identity inside the active Binder IPC transaction
enforceTrustedCaller()
// Safe to proceed with sensitive operations
}
}
override fun onBind(intent: Intent): IBinder {
// Return the AIDL Stub. Do NOT perform Binder.getCallingUid() checks in onBind()
// because onBind() is invoked asynchronously by system_server and the IBinder is cached.
return binder
}
private fun enforceTrustedCaller() {
val callingUid = Binder.getCallingUid()
if (callingUid == Process.myUid() || !verifyUidSignature(callingUid)) {
throw SecurityException("Access Denied: Unauthorized or local/cleared-identity caller.")
}
}
private fun verifyUidSignature(callingUid: Int): Boolean {
return try {
val trustedSha256Raw = Base64.decode(TRUSTED_PARTNER_SHA256, Base64.DEFAULT)
// PackageManager.hasSigningCertificate(uid, ...) (API 28+) checks the UID directly
// and handles certificate rotation.
packageManager.hasSigningCertificate(
callingUid,
trustedSha256Raw,
PackageManager.CERT_INPUT_SHA256
)
} catch (e: IllegalArgumentException) {
Log.e("SECURITY_ERROR", "Invalid certificate hash format", e)
false
}
}
}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)
}
}When querying a ContentProvider on the client side, catch SQLiteException
and log a generic error message rather than exposing raw query syntax details
to the user interface.
Reporting guidelines
When this skill is executed to apply security hardening updates to a codebase, the agent MUST generate a structured "Security Hardening and 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.
Security hardening and alignment update template
Use the following Markdown template when reporting changes to developers:
### Security hardening and 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
1. [Step 1 to verify the component behaves correctly, for example, run
component unit test]
2. [Step 2 to verify regression safety]
Quality checklist
NEVER
- NEVER launch a nested
Intentreceived from an untrusted source without verifying that its resolved component (packageNameandclassName) matches an explicit allowlist and rejecting or stripping URI permission grant flags. - 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.
DO
- 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 permissions or use
RECEIVER_NOT_EXPORTEDfor dynamic receivers to restrict the sender. - MUST declare the
${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSIONsignature<permission>and<uses-permission>inAndroidManifest.xmlwhenminSdk < 33. - 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 and update methods. - MUST verify the caller's signing certificate on every IPC call in exported services using an AIDL
StuborMessenger. - MUST use
androidx.core.content.IntentSanitizerto sanitize incoming dynamic intents before redirection, if AndroidX Core 1.9.0+ is imported in the project.
FAQ
What does Android Intent Security do?
Audits, detects, and remediates Android Intent and component security vulnerabilities. Use this skill when auditing component configurations in AndroidManifest.xml (activities, services, receivers) or source code handling incoming Intents (getIntent, getParcelableExtra) to prevent Intent Redirection and unauthorized access. Don't use for network security, TLS configuration, or general runtime permission request flows.
Who made Android Intent Security?
android (an organization) publishes it in the android/skills repository on GitHub.
Is Android Intent Security free?
Yes. Android Intent Security is open source, published by android under the Apache-2.0 license.
What comes with Android Intent Security?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use Android Intent Security?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.