mcpField
A
android-intent-security

android-intent-security

github.com/android安全10.0k1.8k 次安装2 天前更新
代码仓库

关于 android-intent-security

面向 Android 开发的安全技能,提供组件与 Intent 的防护规则和实现模式,重点处理不可信 Intent 转发、PendingIntent、ContentProvider 及跨进程调用。

主要功能

  1. 01安全处理 Intent 路由与转发
  2. 02使用 IntentSanitizer 校验输入
  3. 03配置 PendingIntent 可变性
  4. 04保护 ContentProvider 权限
  5. 05验证导出服务的调用方签名

适用场景

  1. 01审查 Android 组件的导出与权限配置
  2. 02防止不可信 Intent 重定向
  3. 03安全创建通知或闹钟 PendingIntent
  4. 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 PackageManager API。

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 的嵌套 Intent extra 的传入 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())。
  • 预期输出:
    • 如果调用方 UID 的签名证书与可信合作方哈希匹配,则执行所请求的服务操作;否则抛出 SecurityException。
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 必须为开发者生成一份结构化的“最佳实践与安全对齐更新”报告。报告必须写入会话产物文件夹(或在最终回复中打印),并包括:

  1. 安全对齐领域: 所应用改进的类别(例如,安全 Intent 重定向、安全 PendingIntent 配置、ContentProvider 数据防护)。
  2. 影响和优先级: 此更新所应对的潜在安全风险(例如,防止组件劫持、隔离私有数据)。
  3. 变更范围: 所有修改过的类、XML 文件和依赖项的列表。
  4. 实现摘要: 解决方案的具体细节(例如,“已更新嵌套 Intent 解析,使用 IntentSanitizer API 和严格的组件允许列表”)。
  5. 代码差异: 显示确切修改内容的标准统一差异。
最佳实践与安全对齐更新模板

报告变更时,请使用以下 Markdown 模板告知开发者:

### 最佳实践与安全对齐更新:[安全对齐领域]

*   **改进说明:**[简要说明加固更新的内容及推荐原因]
*   **优先级:**[高 / 中 / 低]
*   **对齐操作:**[更新概要,例如,已转换为 FLAG_IMMUTABLE]

#### 修改的文件
*   `[文件 1 的相对路径]`
*   `[文件 2 的相对路径]`

#### 实现差异
```diff
// 在此处插入统一差异
测试与验证
  1. [验证组件行为正确的步骤 1,例如,运行组件单元测试]
  2. [验证回归安全性的步骤 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,然后再进行重定向。

评论

支持 Markdown