关于 android-intent-security
面向 Android 开发的安全技能,提供组件与 Intent 的防护规则和实现模式,重点处理不可信 Intent 转发、PendingIntent、ContentProvider 及跨进程调用。
主要功能
- 01安全处理 Intent 路由与转发
- 02使用 IntentSanitizer 校验输入
- 03配置 PendingIntent 可变性
- 04保护 ContentProvider 权限
- 05验证导出服务的调用方签名
适用场景
- 01审查 Android 组件的导出与权限配置
- 02防止不可信 Intent 重定向
- 03安全创建通知或闹钟 PendingIntent
- 04限制跨应用服务调用及数据访问
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ android/ skills --skill android-intent-security安装后在 Claude Code 中输入/android-intent-security确认已加载。
详细介绍
本技能提供用于保护 Android 组件 (Activity、Service、Broadcast Receiver、Content Provider)并安全处理 Intent 的指南和模式,以防止权限提升和未授权访问。
术语表
- Intent: 用于请求另一个应用组件执行操作的异步消息对象。
- 已导出组件: 可由设备上的其他应用启动的组件(
android:exported="true")。 - 粘性 Intent: 发送后仍保留在系统缓存中的广播 Intent,允许任何应用获取其内容。
- 签名权限: 保护级别设为
signature的权限,仅授予使用相同开发者密钥签名的应用。 - onNewIntent: 当 Activity 以
FLAG_ACTIVITY_SINGLE_TOP启动,且已在历史栈顶部运行时调用的 Activity 生命周期回调。 - PendingIntent: 授予其他应用(例如系统服务)的令牌,允许其使用创建者的权限执行预定义的 Intent。
- 可变 PendingIntent: 底层 Intent 参数可由接收应用修改的 PendingIntent。
- ContentProvider: 封装数据并通过标准查询/插入接口向其他应用提供数据的组件。
- IntentSanitizer: AndroidX Core 中的实用类,用于通过过滤未授权的组件、操作或额外数据,构建安全且经过净化的传入 Intent 副本。
- Intent 重定向(转发): 应用从不可信来源接收 Intent,并用它启动私有的非导出组件时出现的漏洞。
前提条件
- Agent 必须能够说明
onCreate、onNewIntent和singleTop启动模式的功能及其安全影响。 - Agent 必须能够在
AndroidManifest.xml中声明<activity>、<service>、<receiver>和<provider>标签,并定义其android:exported和android:permission属性。 - Agent 必须能够使用
PackageManager实现签名验证检查。
限制
- 本技能侧重于 Android 平台上的本地组件间和应用间通信安全。
- 本技能不涵盖网络安全、Web 集成或主机到服务器的安全。
设置和依赖项
- Android SDK: 标准硬件支持的密钥库操作和组件验证要求最低 API 级别为 23(Android 6.0)。
- AndroidX Core Library: 使用
IntentSanitizer必须依赖androidx.core:core:1.9.0或更高版本。 - 标准 API 访问: 运行时组件验证需要使用标准 Android
PackageManagerAPI。
Intent 安全逻辑和决策
1. Intent 路由对比
评估不同 Intent 传递方式的安全特性:
| Intent 传递方式 | 范围 | 推荐用例 |
|---|---|---|
| 显式 Intent(内部) | 应用私有 | 启动内部 Activity/Service |
| 隐式 Intent | 系统范围 | 启动系统相机、拨号器或分享功能 |
| 本地广播(LocalBroadcastManager)(已弃用) | 应用私有 | 内部异步事件路由。已弃用:请改用应用内观察者,例如 Kotlin Flows/SharedFlow、LiveData 或响应式模式。 |
| 系统广播 | 系统范围 | 接收系统事件(NFC、Bluetooth) |
2. PendingIntent 可变性标志选项
评估 PendingIntent 可变性标志的安全影响:
| 标志名称 | 可变性 | 推荐用例 |
|---|---|---|
PendingIntent.FLAG_IMMUTABLE | 不可变 | 几乎所有 PendingIntent 的默认选项,例如闹钟和通知 |
PendingIntent.FLAG_MUTABLE | 可变 | 通知内联回复、切片操作(需要显式目标 Intent) |
3. 意图处理与重定向逻辑
IF(组件将嵌套 Intent 作为 extra 接收){
IF(可用 AndroidX Core 1.9.0+ 或更高版本){
MUST 构造 IntentSanitizer,明确将组件、操作、数据和 extras 列入允许列表。
MUST 在启动前调用 sanitizeByThrowing() 或 sanitizeByFiltering()。
} ELSE {
MUST 拒绝或剥离 URI 权限授予标志(FLAG_GRANT_READ_URI_PERMISSION、FLAG_GRANT_WRITE_URI_PERMISSION、FLAG_GRANT_PERSISTABLE_URI_PERMISSION、FLAG_GRANT_PREFIX_URI_PERMISSION)。
MUST 使用 resolveActivity() 解析目标组件,并验证其 packageName 和 className 均与明确列入允许列表的组件匹配。
}
NEVER 未经验证直接启动嵌套 Intent。
} ELSE IF(组件处理广播){
MUST 依赖系统的受保护广播机制处理系统事件(该机制可确保发送方是系统框架)。
MUST 使用签名级权限保护自定义接收器,或对动态接收器使用 RECEIVER_NOT_EXPORTED,以限制发送方。
IF(使用 ContextCompat.registerReceiver 和 RECEIVER_NOT_EXPORTED,且 minSdk < 33){
MUST 验证 AndroidManifest.xml 中是否声明了 <permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature" /> 和 <uses-permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />。
}
}
4. PendingIntent 安全逻辑
IF(创建 PendingIntent 并将其传递给其他应用){
MUST 默认使用 PendingIntent.FLAG_IMMUTABLE。
IF(PendingIntent 必须可变){
MUST 在基础 Intent 中设置明确的目标组件或包名。
NEVER 创建隐式且可变的 PendingIntent。
}
}
5. ContentProvider 安全逻辑
IF(ContentProvider 仅供应用内部使用){
MUST 设置 android:exported="false"。
} ELSE {
MUST 使用 android:readPermission 和 android:writePermission 保护它。
MUST 设置 android:grantUriPermissions="false",除非确实需要临时 URL 访问权限。
}
6. 服务调用方验证逻辑
IF(导出的服务与受信任的关联应用或合作伙伴应用通信){
MUST 暴露跨进程接口(例如 AIDL Stub 或 Messenger),而不是仅在同一进程内工作的本地 Binder 子类。
MUST 验证每个 IPC 请求的调用方——在 AIDL Stub 方法(或 onTransact())中使用 Binder.getCallingUid(),或在 Messenger Handler 中使用 Message.sendingUid——并验证调用包的签名指纹是否与受信任的证书哈希匹配。
}
代码和配置模式
1. 安全的意图重定向(手动验证)
当无法使用现代清理库时,在启动前验证嵌套 intent 的目标。
- 预期输入:
- 包含名为
EXTRA_NESTED_INTENT的嵌套Intentextra 的传入Intent。
- 包含名为
- 预期输出:
- 若目标组件安全,则启动该组件;若验证失败,则抛出
SecurityException。
- 若目标组件安全,则启动该组件;若验证失败,则抛出
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. 使用 IntentSanitizer 安全地重定向意图
使用 AndroidX IntentSanitizer(AndroidX Core
1.9.0+)过滤或拒绝动态 intent。
- 预期输入:
- 不受信任的传入
Intent。
- 不受信任的传入
- 预期输出:
Intent:仅包含列入允许列表的组件、类别和操作的清理后副本。若使用sanitizeByThrowing(),违反规则时会抛出SecurityException。
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. 自定义签名权限保护
在清单中声明自定义签名级权限,以保护同系列应用之间的通信。
- 预期输入: 清单配置。
- 预期输出: 只能由使用同一开发者证书签名的应用启动的 activity。
<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. 安全的 onNewIntent 生命周期验证(热启动保护)
确保复用动态 Intent 的 Activity(例如,在后台启动路径中)在 onNewIntent 中应用同样严格的安全过滤器。
- 预期输入:
newIntent(Intent):新传入的 Intent。
- 预期输出:
- 仅当新 Intent 通过安全验证时,才执行处理逻辑。
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. 安全创建 PendingIntent
除非明确需要可变性,否则应强制设为不可变。
- 预期输入(不可变): Intent 目标。
- 预期输出(不可变): 接收方无法更改的
PendingIntent。 - 预期输入(可变): 已设置显式组件的 Intent。
- 预期输出(可变): 锁定到特定接收方组件的可变
PendingIntent,以防止劫持。
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. 安全配置 ContentProvider 并执行查询
安全地公开 ContentProvider,并使用参数化查询来防止 SQL 注入。
- 预期输入:
uri(Uri):查询 URI。projection(String[]):要检索的列。selection(String):查询条件。selectionArgs(String[]):与 selection 占位符(?)对应的值。
- 预期输出:
Cursor:经过筛选的查询结果,严格限制为 projection 映射中的列。
<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. 验证服务调用方签名
在已导出的绑定服务中,对传入的跨进程 IPC 调用验证调用应用的签名证书。
- 预期输入:
- 传入的跨进程 Binder 事务(例如,AIDL
Stub方法调用或onTransact())。
- 传入的跨进程 Binder 事务(例如,AIDL
- 预期输出:
- 如果调用方 UID 的签名证书与可信合作方哈希匹配,则执行所请求的服务操作;否则抛出
SecurityException。
- 如果调用方 UID 的签名证书与可信合作方哈希匹配,则执行所请求的服务操作;否则抛出
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
}
}
}
错误处理
安全地处理组件绑定、数据库查询和 Intent 重定向失败,避免暴露内部结构。
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)
}
}
// 在客户端安全处理 ContentProvider 查询:
try {
val cursor = contentResolver.query(providerUri, projection, selection, selectionArgs, null)
} catch (e: SQLiteException) {
Log.e("PROVIDER_ERROR", "ContentProvider 数据库查询失败", e)
// 安全处理:防止将原始查询语法细节泄露到 UI
}
报告指南
执行此技能以对代码库应用安全加固更新时,Agent 必须为开发者生成一份结构化的“最佳实践与安全对齐更新”报告。报告必须写入会话产物文件夹(或在最终回复中打印),并包括:
- 安全对齐领域: 所应用改进的类别(例如,安全 Intent 重定向、安全 PendingIntent 配置、ContentProvider 数据防护)。
- 影响和优先级: 此更新所应对的潜在安全风险(例如,防止组件劫持、隔离私有数据)。
- 变更范围: 所有修改过的类、XML 文件和依赖项的列表。
- 实现摘要: 解决方案的具体细节(例如,“已更新嵌套 Intent 解析,使用
IntentSanitizerAPI 和严格的组件允许列表”)。 - 代码差异: 显示确切修改内容的标准统一差异。
最佳实践与安全对齐更新模板
报告变更时,请使用以下 Markdown 模板告知开发者:
### 最佳实践与安全对齐更新:[安全对齐领域]
* **改进说明:**[简要说明加固更新的内容及推荐原因]
* **优先级:**[高 / 中 / 低]
* **对齐操作:**[更新概要,例如,已转换为 FLAG_IMMUTABLE]
#### 修改的文件
* `[文件 1 的相对路径]`
* `[文件 2 的相对路径]`
#### 实现差异
```diff
// 在此处插入统一差异
测试与验证
- [验证组件行为正确的步骤 1,例如,运行组件单元测试]
- [验证回归安全性的步骤 2] ```
反模式
- 绝不在未验证其解析后的组件(
packageName和className)与明确的允许列表匹配,并拒绝或剥除 URI 权限授予标志的情况下,启动从不可信来源接收的嵌套Intent。 - 绝不使用粘性广播(
sendStickyBroadcast)。 - 绝不认为导出的组件是安全的,仅仅因为它在后台线程中运行或执行了内部检查。
- 绝不向未受签名级权限限制的组件暴露敏感功能(例如 SSO 身份验证或支付处理)。
- 绝不在
onNewIntent中处理传入的 intent,却不应用与onCreate相同的安全控制。 - 绝不在未为基础
Intent设置明确目标组件的情况下,创建可变的PendingIntent。 - 绝不使用动态字符串拼接在
ContentProvider查询中构造选择条件。 - 绝不在
BroadcastReceiver.onReceive中使用Binder.getCallingUid来识别广播发送方,因为它返回的是接收方自身的 UID,而不是发送方的 UID。
最佳实践
- 必须为所有不需要外部通信的组件明确设置
android:exported="false"。 - 必须在系列应用之间通信时,使用
android:protectionLevel="signature"为所有导出的组件配置自定义权限进行保护。 - 必须验证所有传入的 intent 附加数据,并妥善处理缺失的参数,以防止崩溃。
- 必须依靠系统的 受保护广播机制处理系统事件(例如,启动完成、软件包变更),因为系统会阻止不可信应用伪造这些操作。
- 必须使用签名级权限保护自定义广播,或对动态接收器使用
RECEIVER_NOT_EXPORTED来限制发送方身份(当minSdk < 33时,使用ContextCompat.registerReceiver还必须确认AndroidManifest.xml中包含<permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature" />和<uses-permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />)。 - 必须在
onNewIntent()中调用setIntent(newIntent),然后再处理负载,以保持活动引用更新。 - 必须在构造
PendingIntent实例时默认使用PendingIntent.FLAG_IMMUTABLE。 - 必须使用
readPermission和writePermission保护导出的ContentProvider。 - 必须在
ContentProvider的 query/update 方法中强制使用参数化选择结构。 - 必须在导出的绑定服务内部的运行时,通过跨进程接口(例如 AIDL
Stub或Messenger)验证传入 IPC 事务的调用应用签名证书。 - 必须在项目引入 AndroidX Core 1.9.0+ 时,使用
androidx.core.content.IntentSanitizer清理传入的动态 intent,然后再进行重定向。