mcpField
A
android-permissions-security

android-permissions-security

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

关于 android-permissions-security

面向 Android 开发者的安全审计技能,指导发现权限配置和 IPC 边界中的漏洞,并给出最小权限修复做法。内容涵盖运行时权限、组件保护、URI 授权及调用方身份验证。

主要功能

  1. 01审计权限与 IPC 漏洞
  2. 02配置自定义权限保护级别
  3. 03限制导出组件和 URI 授权
  4. 04通过 Binder UID 验证调用方
  5. 05处理运行时权限请求

适用场景

  1. 01检查 ContentProvider 的读写权限划分
  2. 02修复可伪造的调用方身份检查
  3. 03收紧内容 URI 的授权范围
  4. 04改进运行时权限拒绝处理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/android/skills --skill android-permissions-security

安装后在 Claude Code 中输入/android-permissions-security确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/android/guide/topics/manifest/
permission-element.md
references/android/guide/topics/permissions/
overview.md
references/android/privacy-and-security/risks/
sender-of-pending-intents.md
references/android/training/permissions/
requesting.md

详细介绍

Android 生态系统中的权限漏洞审计、最小权限原则落实、IPC 边界防护以及运行时权限处理指南。

术语表

  • 运行时权限: 危险权限(例如 Camera、Location),在 Android 6.0+(API 23+)上必须由用户在运行时授予。
  • 自定义权限: 应用定义的权限(<permission>),用于限制对其已导出组件的访问。
  • 保护级别: 定义操作系统如何授予权限的属性(normal、dangerous、signature、knownSigner)。
  • knownSigner: 保护标志(API 31+),向证书摘要与可信证书摘要匹配的合作应用授予签名级访问权限。
  • SigningInfo: 对象(API 28+),提供应用的签名历史和多个签名证书。
  • Binder 调用方身份: 使用 Binder.getCallingUid() 获取的、可靠地代表调用进程的 UID。
  • URI 权限: 使用 grantUriPermissions 和 Intent 标志授予的、对内容 URI 的临时细粒度访问权限。

前置条件

  • 在应用级 build.gradle 文件中添加 androidx.core:core-ktx 和 androidx.activity:activity-ktx(或 androidx.fragment:fragment-ktx)依赖,以使用 ContextCompat 和 ActivityResultContracts。
  • 对于 knownSigner 合作方权限(android:knownCerts),确保项目的 compileSdk 和 targetSdk 至少为 31(Android 12)。
  • 对于广播身份共享(BroadcastOptions.setShareIdentityEnabled),确保 compileSdk 和 targetSdk 至少为 34(Android 14)。

1. 安全审计和权限缺口检测指南

审计 Android 项目的权限和 IPC 安全漏洞时,请系统地检查各个方面:

漏洞领域搜索位置 / 模式风险修复措施
自定义权限保护薄弱AndroidManifest.xml 中的 android:protectionLevel="normal"、"dangerous" 或 "signatureOrSystem"未经授权的应用可不受限制地请求或声明权限升级为 signature 或 `signature
URI 权限授予范围过广<provider> 中的 <grant-uri-permission android:pathPrefix="/" /> 或 pathPrefix=""向第三方泄露提供程序的全部数据替换为明确限定范围的子路径(例如 android:pathPrefix="/shared/")。保留 android:grantUriPermissions="true"。移除所有通配符 pathPrefix="/"。
ContentProvider 缺少读写权限分离<provider> 仅配置通用 android:permission,或未配置 android:readPermission 和 android:writePermission只读调用方执行写操作,或只写调用方访问读取操作配置细粒度的 android:readPermission="...READ_DATA" 和 android:writePermission="...WRITE_DATA",并在 <manifest> 根节点声明带有 android:protectionLevel="signature"(或 `signature
可伪造的调用方身份检查Service 中的 intent.getStringExtra("calling_package")、intent.getStringExtra("sender")、callingPackage == "..."恶意调用方伪造 Intent extra 或字符串移除字符串检查;使用 Binder.getCallingUid() 和 pm.hasSigningCertificate() 以加密方式验证调用方身份。
绑定服务权限绕过Binder AIDL stub 中的 checkCallingOrSelfPermission() 或 enforceCallingOrSelfPermission()在 IPC 外或身份被清除时,会回退到宿主应用 UID,导致“困惑代理”绕过替换为 enforceCallingPermission("...WRITE_DATA", ...) 或 checkCallingPermission("...WRITE_DATA")。
绑定服务缺少方法级检查修改状态的绑定服务 AIDL 方法未强制检查写入权限具有只读绑定访问权限的调用方可执行会修改数据的写操作在会修改数据的 AIDL 方法中添加 enforceCallingPermission("...WRITE_DATA", "Caller lacks WRITE_DATA permission")。
运行时权限流程有缺陷缺少 shouldShowRequestPermissionRationale()、缺少永久拒绝处理,或在循环中重新请求权限用户体验不佳、无限循环或无法恢复权限实现三状态流程:检查是否已授权 -> 检查是否需要显示说明对话框 -> 启动契约。永久拒绝时,跳转至 Settings.ACTION_APPLICATION_DETAILS_SETTINGS。
同时请求位置权限在一次请求中使用 arrayOf(ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION)Android 11+(API 30+)会拒绝首先请求前台位置权限(ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION)。仅在前台权限获批后,通过单独的用户操作请求 ACCESS_BACKGROUND_LOCATION。
媒体存储权限范围过广为选择用户图片而请求 READ_EXTERNAL_STORAGE 或 READ_MEDIA_IMAGES权限过度,侵犯用户隐私迁移到零权限 Photo Picker(ActivityResultContracts.PickVisualMedia)。
缓存权限状态将权限状态存储在 var cachedLocationPermissionGranted = ... 中权限被撤销或过期时会失效在每次调用时动态查询 ContextCompat.checkSelfPermission(),并使用 try-catch 块包裹受保护的调用,以捕获 SecurityException。
不安全的广播调用 sendBroadcast(intent) 时未设置目标包或接收方权限;接收方未验证发送方身份数据被拦截或命令被伪造设置显式包(intent.setPackage(...)),传递接收方权限,并启用 BroadcastOptions.setShareIdentityEnabled(true)(API 34+)以验证 sentFromPackage。

2. 自定义权限与保护级别

在清单中声明自定义权限或合作伙伴证书时,必须遵循 <permission> 元素指南中的说明。

  • signature:限制只有使用相同开发者密钥签名的应用才能访问。
  • signature|knownSigner(API 31+):限制只有签名证书摘要与 android:knownCerts 中声明的哈希值匹配的合作伙伴应用才能访问。
  • normal 或 dangerous:不要用于私有的应用间 IPC。
  • signatureOrSystem:已弃用(API 23)。将已弃用的 signatureOrSystem 权限升级为 signature。避免在非系统应用中使用。
清单片段
签名级别的自定义权限

通过在清单中声明 <permission> 并对目标组件应用 android:permission,使用 signature 权限将组件访问限制为仅允许使用相同开发者证书签名的应用。

<permission
    android:name="com.example.snippets.permission.ACCESS_SECURE_API"
    android:protectionLevel="signature" />

KnownSigner 合作伙伴权限(API 31+)

通过在清单中声明带有 android:knownCerts 的 <permission>,并让其引用可信 SHA-256 证书摘要的资源数组,使用 signature|knownSigner 权限向指定的外部合作伙伴应用授予访问权限。

<resources xmlns:tools="http://schemas.android.com/tools">
    
    <string-array name="trusted_partner_certs" tools:ignore="Typos">
        <item>103938ee4537e59e8ee792f654504fb8346fc6b346d0bbc4415fc339fcfc8ec1</item>
    </string-array>
</resources>

<permission
    android:name="com.example.snippets.permission.PARTNER_API"
    android:protectionLevel="signature|knownSigner"
    android:knownCerts="@array/trusted_partner_certs" />


3. 导出组件保护与 URI 权限

  • 私有组件:始终设置 android:exported="false"。
  • 导出组件:要求设置权限(android:permission、android:readPermission、android:writePermission)。
  • 细粒度 URI 授权:保留 android:grantUriPermissions="true" 以支持安全共享,但将授权范围限定为特定子路径。切勿使用通配符 pathPrefix="/"。
  • 自定义权限声明:在 Provider 上拆分读写权限时,必须在 <manifest> 根级别声明带有 android:protectionLevel="signature"(或 signature|knownSigner)的 <permission> 标签。
  • 修复现有通配符授权:必须查找并移除所有现有的 <grant-uri-permission android:pathPrefix="/" /> 或 pathPrefix="",并将其替换为明确限定范围的子路径(例如 android:pathPrefix="/shared/")。
  • 细粒度 UriPermission 管理:不要使用宽泛的清单授权,而应使用运行时 android.content.UriPermission 令牌,以遵循最小权限原则按 URI 共享:
    • 授予临时访问权限:context.grantUriPermission(targetPackage, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION),或将 Intent.FLAG_GRANT_READ_URI_PERMISSION 附加到发出的 Intent。
    • 持久化访问权限:接收应用调用 contentResolver.takePersistableUriPermission(uri, modeFlags),以便在重启后仍能持续访问。
    • 审核活动授权:检查 contentResolver.persistedUriPermissions(List<UriPermission>),验证已授权的 URI(uriPermission.uri、uriPermission.isReadPermission()、uriPermission.isWritePermission())。
    • 撤销访问权限:操作完成后,立即调用 context.revokeUriPermission(uri, modeFlags) 或 contentResolver.releasePersistableUriPermission(uri, modeFlags)。
清单片段:完整的安全 ContentProvider 声明

在 <manifest> 根级别声明拆分后的读写权限:

<permission
    android:name="com.example.snippets.permission.READ_DATA"
    android:protectionLevel="signature" />
<permission
    android:name="com.example.snippets.permission.WRITE_DATA"
    android:protectionLevel="signature" />

然后使用这些权限配置 Provider,并设置限定范围的 URI 授权:

<provider
    android:name=".permissions.SecureDataProvider"
    android:authorities="com.example.snippets.partnerprovider"
    android:exported="true"
    android:readPermission="com.example.snippets.permission.READ_DATA"
    android:writePermission="com.example.snippets.permission.WRITE_DATA"
    android:grantUriPermissions="true">
    <grant-uri-permission android:pathPrefix="/shared/" />
</provider>

运行时细粒度 UriPermission 管理片段
object UriPermissionManager {

    /** Grants temporary read access to a specific URI for a partner package. */
    fun grantScopedUriAccess(context: Context, targetPackage: String, uri: Uri) {
        context.grantUriPermission(
            targetPackage,
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION
        )
    }

    /** Audits active persisted URI permissions held by the application. */
    fun getActivePersistedPermissions(context: Context): List<UriPermission> {
        return context.contentResolver.persistedUriPermissions
    }

    /** Revokes temporary URI access when data transfer completes. */
    fun revokeScopedUriAccess(context: Context, uri: Uri) {
        context.revokeUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
    }
}


4. 以密码学方式验证调用方身份

绝不要信任 Intent 或字符串参数中提供的调用方软件包名称 (getCallingPackage()、intent.getStringExtra("calling_package")、 intent.getStringExtra("sender")、intent.getStringExtra("package"))。恶意应用可以伪造 Intent 额外数据和字符串形式的软件包名称。

严重漏洞:利用 Intent 额外数据伪造软件包名称
// VULNERABLE PATTERN: DO NOT DO THIS
val callingPackage = intent?.getStringExtra("calling_package")
if (callingPackage != null && SignatureUtils.verifyPartnerPackage(this, callingPackage)) {
    // A malicious app passes "com.example.partner" in the extra.
    // The signature check verifies the installed partner on disk, but the CALLER was malicious!
}

### 安全模式:以加密方式验证调用方 UID
  1. 彻底移除所有 intent.getStringExtra("calling_package")、intent.getStringExtra("sender") 和 callingPackage == "..." 检查。
  2. 在活动的 Binder IPC 事务中(例如 AIDL Stub 方法或 ContentProvider 方法)使用 Binder.getCallingUid() 获取调用方身份。
  3. 使用 PackageManager.hasSigningCertificate()(API 28+)或 PackageManager.getPackagesForUid() 验证调用方证书。
完整的 CallerVerifier 实现

将此实现应用于当前执行易被伪造检查的服务。

  • 放置位置:在你要修复的服务的同一个文件中声明 CallerVerifier(例如,在 SecureDataService.kt 内)。不要将验证逻辑移到新文件中,而让原服务保持不变。
  • 活动的 Binder IPC 上下文:Binder.getCallingUid() 仅在活动的 Binder 事务期间返回远程调用方的 UID(绝不能在 Service.onStartCommand() 或 Service.onBind() 中调用;此时它会返回 Process.myUid())。拒绝 callingUid == Process.myUid(),确保在活动 IPC 事务之外或调用 Binder.clearCallingIdentity() 之后的调用会以失败关闭,而不是绕过验证。
  • 可空性:packageInfo.signatures 的类型为 Array<Signature>?。请严格照抄下方的 ?: return false 保护语句。直接遍历 packageInfo.signatures 会因“Non-nullable value required to call an 'iterator()' method in a for-loop.”而无法编译。
object CallerVerifier {
    private const val TRUSTED_PARTNER_SHA256 =
        "A1B2C3D4E5F60708090A0B0C0D0E0F1011121314151617181920212223242526"

    fun isCallerAuthorized(context: Context): Boolean {
        val callingUid = Binder.getCallingUid()
        if (callingUid == android.os.Process.myUid()) return false

        val pm = context.packageManager
        // Modern API 28+ check by UID:
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            val certBytes = hexStringToByteArray(TRUSTED_PARTNER_SHA256)
            return pm.hasSigningCertificate(callingUid, certBytes, PackageManager.CERT_INPUT_SHA256)
        }

        // Fallback for legacy APIs:
        val callingPackages = pm.getPackagesForUid(callingUid) ?: return false
        for (pkg in callingPackages) {
            if (verifyPackageSignature(pm, pkg)) {
                return true
            }
        }
        return false
    }

    @Suppress("DEPRECATION")
    private fun verifyPackageSignature(pm: PackageManager, packageName: String): Boolean {
        return try {
            val packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
            val signatures = packageInfo.signatures ?: return false
            for (sig in signatures) {
                val digest = java.security.MessageDigest.getInstance("SHA-256").digest(sig.toByteArray())
                val hex = digest.joinToString("") { "%02X".format(it) }
                if (hex.equals(TRUSTED_PARTNER_SHA256, ignoreCase = true)) return true
            }
            false
        } catch (e: PackageManager.NameNotFoundException) {
            false
        }
    }

    private fun hexStringToByteArray(s: String): ByteArray {
        val len = s.length
        val data = ByteArray(len / 2)
        for (i in 0 until len step 2) {
            data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
        }
        return data
    }
}


5. 绑定服务中的方法级 Binder 强制执行(SecureBoundService.kt)

对于公开 AIDL Binder 存根的绑定服务,请使用 enforceCallingPermission() 保护会修改状态的方法。

关键安全规则:绝不要在服务中使用 CallingOrSelfPermission
  • 绝不要使用 checkCallingOrSelfPermission() 或 enforceCallingOrSelfPermission()。
  • **原因:**在活动的 Binder 事务期间,此方法会检查调用方的权限。但是,如果在本地调用该方法(不在活动 IPC 期间),或 IPC 调用身份已被清除,该方法就会退而检查宿主应用的权限。由于宿主应用拥有该权限,这种回退会授予访问权限并绕过安全边界,从而造成 Confused Deputy 漏洞。
  • 必须使用:enforceCallingPermission("com.example.snippets.permission.WRITE_DATA", "Caller lacks WRITE_DATA permission") 或 checkCallingPermission("com.example.snippets.permission.WRITE_DATA")。
完整的 SecureBoundService 实现
class SecureBoundService : Service() {

    private val binder = object : IMyService.Stub() {
        override fun getData(): String {
            // Read-only operation guarded by manifest-level permission
            return "Confidential Data"
        }

        override fun modifyData(newData: String) {
            // MUST use enforceCallingPermission or checkCallingPermission.
            // NEVER use checkCallingOrSelfPermission or enforceCallingOrSelfPermission.
            [email protected](
                "com.example.snippets.permission.WRITE_DATA",
                "Caller lacks WRITE_DATA permission"
            )
            updateInternalState(newData)
            Log.d("SecureBoundService", "Data modified to: $newData with proper WRITE_DATA permission check")
        }
    }

    override fun onBind(intent: Intent?): IBinder = binder

    private fun updateInternalState(data: String) {
        // Internal state update logic
    }
}


6. 安全的运行时权限工作流(RuntimePermissionsActivity.kt)

实现运行时权限请求或处理权限拒绝状态时,必须遵循请求运行时权限指南 和权限概览中的说明。实现一个 3 状态流程,不缓存状态,也不创建无限重试循环:

  1. 检查已授权状态:ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED。
  2. 检查理由:使用 shouldShowRequestPermissionRationale(permission) 在请求权限前提供说明。
  3. 启动契约:ActivityResultContracts.RequestPermission()。
  4. 处理永久拒绝:在回调中,如果权限被拒绝且 !shouldShowRequestPermissionRationale(permission),请引导用户前往 Settings.ACTION_APPLICATION_DETAILS_SETTINGS。
完整的 RuntimePermissionsActivity.kt 实现
class RuntimePermissionsActivity : ComponentActivity() {

    private val cameraLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                startCameraPreview()
            } else {
                if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
                    // User selected 'Don't ask again' or permanently denied.
                    // Direct user to Application Details Settings.
                    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                        data = Uri.fromParts("package", packageName, null)
                    }
                    startActivity(intent)
                } else {
                    showSnackbar("Camera permission is required to preview camera feed.")
                }
            }
        }

    fun requestCameraPermissionSafely() {
        val permission = Manifest.permission.CAMERA
        when {
            ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -> {
                startCameraPreview()
            }
            shouldShowRequestPermissionRationale(permission) -> {
                showSnackbar("Camera permission is needed to preview camera feed.")
                cameraLauncher.launch(permission)
            }
            else -> {
                cameraLauncher.launch(permission)
            }
        }
    }

    private fun startCameraPreview() {
        Log.d("RuntimePermissionsActivity", "Camera preview started")
    }

    private fun showSnackbar(msg: String) {
        Log.i("RuntimePermissionsActivity", msg)
    }
}


7. 零权限替代方案和顺序流程

实现照片或媒体选择时,必须遵循照片选择器 API 指南中的说明,避免请求广泛的存储权限。

  • 照片选择器:优先使用 ActivityResultContracts.PickVisualMedia(),而不是请求广泛的存储权限(READ_MEDIA_IMAGES、READ_EXTERNAL_STORAGE)。
  • 顺序位置请求流程:首先请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION。切勿同时请求 ACCESS_BACKGROUND_LOCATION。只有在获得前台位置权限后,才能在单独的步骤中请求后台位置权限。
照片选择器实现
private val photoPickerLauncher =
    registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
        if (uri != null) {
            handleImageUri(uri)
        }
    }

fun selectPhoto() {
    photoPickerLauncher.launch(
        PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
    )
}

顺序位置请求实现
private val foregroundLocationLauncher =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        val fineGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
        val coarseGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false
        if (fineGranted || coarseGranted) {
            startForegroundLocationUpdates()
        }
    }

fun requestForegroundLocation() {
    foregroundLocationLauncher.launch(
        arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )
    )
}

// Background location requested only AFTER foreground is granted and user explicitly opts in:
private val backgroundLocationLauncher =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        if (isGranted) {
            startBackgroundTracking()
        }
    }


8. 无状态检查和错误边界

  • 切勿缓存权限状态到内存变量中(例如 cachedLocationPermissionGranted)。每次操作前都应动态调用 ContextCompat.checkSelfPermission() 进行检查。
  • 使用 try-catch 包装 API 调用:在受权限保护的框架调用(例如位置查询)周围捕获 SecurityException,以妥善处理一次性权限被撤销的情况。
fun performLocationAccess() {
    val fine = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
    val coarse = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
    if (fine != PackageManager.PERMISSION_GRANTED && coarse != PackageManager.PERMISSION_GRANTED) {
        requestForegroundLocation()
        return
    }

    val provider = if (fine == PackageManager.PERMISSION_GRANTED) {
        LocationManager.GPS_PROVIDER
    } else {
        LocationManager.NETWORK_PROVIDER
    }

    try {
        val location = locationManager.getLastKnownLocation(provider)
        processLocation(location)
    } catch (e: SecurityException) {
        Log.e("LocationAccess", "Permission revoked at runtime", e)
    }
}


9. 安全广播(API 34+ 共享身份)

使用显式 Intent、所需的接收器权限或 Android 14+ 广播选项:

// Sender: Enforce permission and share identity
val intent = Intent("com.example.snippets.permission.ACTION_SECRET_UPDATE").apply {
    setPackage("com.example.partner") // Explicit target
}
val isUdc = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
val options = if (isUdc) {
    BroadcastOptions.makeBasic().apply {
        setShareIdentityEnabled(true)
    }.toBundle()
} else null

sendBroadcast(intent, "com.example.snippets.permission.RECEIVE_SECRET_UPDATE", options)

// Receiver: Validate sender on Android 14+
class ProtectedReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == "com.example.snippets.permission.ACTION_SECRET_UPDATE") {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                val sender = sentFromPackage
                if (sender != "com.example.trusted_sender") {
                    return // Reject unauthorized sender
                }
            }
            processUpdate(intent)
        }
    }

    private fun processUpdate(intent: Intent) {}
}

旧版兼容性(API 34 之前):在 API 33 及更低版本上,切勿使用 PendingIntent.getCreatorPackage() 或 PendingIntent.getCreatorUid() 验证广播发送方,因为任何获取了受信任应用 PendingIntent 的应用都能附加内容并发送它(参见Pending Intent 的发送方)。 请改为在清单中使用 signature 或 signature|knownSigner(API 31+)权限限制 <receiver>;如果 minSdkVersion 低于 31,则使用绑定的 Service 或 ContentProvider,并验证 Binder.getCallingUid()。


质量检查清单

应做事项
  • 必须在 <manifest> 根节点为自定义权限和 ContentProvider 拆分权限声明 <permission android:name="..." android:protectionLevel="signature" />(或 signature|knownSigner)。
  • 必须为所有私有组件显式设置 android:exported="false",以免需要管理自定义权限。
  • 必须使用 Binder.getCallingUid() 和证书哈希(PackageManager.hasSigningCertificate 或 getPackagesForUid)以加密方式验证调用方,而不是检查可被伪造的 Intent 额外数据。
  • 必须在运行时权限提示之前和处理拒绝时检查 shouldShowRequestPermissionRationale()。
  • 必须在权限被永久拒绝时引导用户前往 Settings.ACTION_APPLICATION_DETAILS_SETTINGS。
  • 必须在可修改数据的 AIDL 方法上使用 enforceCallingPermission() 或 checkCallingPermission() 执行方法级检查。
  • 必须将 ContentProvider URI 权限限定到明确的子路径(例如 android:pathPrefix="/shared/"),同时保留 android:grantUriPermissions="true"。
  • 必须在受保护的 API 调用周围捕获 SecurityException。
  • 必须按顺序请求前台和后台位置权限,切勿同时请求。
切勿
  • 切勿从 Intent extra 中提取或验证包名(intent.getStringExtra("calling_package")、intent.getStringExtra("sender")、intent.getStringExtra("package"))。始终使用 Binder.getCallingUid() 和证书进行身份验证。
  • 切勿使用 PendingIntent.getCreatorPackage() 或 PendingIntent.getCreatorUid() 对广播或 IPC 发送方进行身份验证。
  • 切勿在保留可伪造的 Intent extra 检查的同时,仅通过添加清单权限来补救调用方伪造问题。
  • 切勿使用 checkCallingOrSelfPermission() 或 enforceCallingOrSelfPermission() 来保护外部调用方方法。始终使用 enforceCallingPermission() 或 checkCallingPermission()。
  • 切勿使用已弃用的 signatureOrSystem。
  • 切勿声明或保留 <grant-uri-permission android:pathPrefix="/" /> 或 pathPrefix=""。始终将其替换为范围受限的子路径。
  • 切勿在权限请求被拒绝后无限循环请求权限。
  • 切勿在同一次请求中同时请求前台和后台位置权限。
  • 切勿在内存字段中缓存权限授予状态。

评论

支持 Markdown