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. 04Binder UID による呼び出し元の検証
  5. 05実行時権限リクエストの処理

活用シーン

  1. 01ContentProvider の読み書き権限の確認
  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

詳細

権限の脆弱性の監査、最小権限の徹底、IPC 境界の保護、Android エコシステム全体での実行時権限の取り扱いに関するガイドラインです。

用語集

  • 実行時権限: Android 6.0 以降(API 23 以降)で、実行時にユーザーによる許可が必要な危険な権限(例:カメラ、位置情報)です。
  • カスタム権限: エクスポートされたコンポーネントへのアクセスを制限するために、アプリが定義する権限(<permission>)です。
  • 保護レベル: OS が権限を付与する方法を定義する属性(normal、dangerous、signature、knownSigner)です。
  • knownSigner: 信頼できる証明書ダイジェストと一致するパートナーアプリに、署名レベルのアクセスを許可する保護フラグ(API 31 以降)です。
  • SigningInfo: アプリケーションの署名履歴と複数の署名者証明書を提供するオブジェクト(API 28 以降)です。
  • Binder 呼び出し元の ID: Binder.getCallingUid() を使用して取得する、呼び出し元プロセスを表す信頼性の高い UID です。
  • URI 権限: grantUriPermissions とインテントフラグを使用して、コンテンツ URI に一時的かつきめ細かなアクセスを許可します。

前提条件

  • ContextCompat と ActivityResultContracts を使用するため、アプリレベルの build.gradle ファイルに androidx.core:core-ktx と androidx.activity:activity-ktx(または androidx.fragment:fragment-ktx)の依存関係を追加してください。
  • knownSigner パートナー権限(android:knownCerts)を使用する場合は、プロジェクトの compileSdk と targetSdk が 31(Android 12)以上であることを確認してください。
  • ブロードキャストでの ID 共有(BroadcastOptions.setShareIdentityEnabled)を使用する場合は、compileSdk と targetSdk が 34(Android 14)以上であることを確認してください。

1. セキュリティ監査と権限の不足検出プレイブック

Android プロジェクトの権限と IPC のセキュリティ脆弱性を監査する際は、 各領域を体系的に調査してください。

脆弱性の領域検索対象 / パターンリスク対処方法
弱いカスタム権限保護AndroidManifest.xml の android:protectionLevel="normal"、"dangerous"、または "signatureOrSystem"権限のないアプリが制限なく権限を要求または取得できますandroid:knownCerts を指定して signature または `signature
過度に広範な URI 権限の付与<provider> 内の <grant-uri-permission android:pathPrefix="/" /> または pathPrefix=""プロバイダーのすべてのデータが第三者に漏えいします明示的に範囲を限定したサブパス(例:android:pathPrefix="/shared/")に置き換えます。android:grantUriPermissions="true" は維持します。ワイルドカードの pathPrefix="/" は削除します。
ContentProvider の権限分離の欠如汎用の android:permission のみが指定されている、または android:readPermission と android:writePermission が設定されていない <provider>読み取り専用の呼び出し元が書き込み操作を実行したり、書き込み専用の呼び出し元が読み取り操作にアクセスしたりします詳細な android:readPermission="...READ_DATA" と android:writePermission="...WRITE_DATA" を設定し、さらにカスタムの <permission> タグを <manifest> のルートで android:protectionLevel="signature"(または `signature
偽装可能な呼び出し元 ID の確認サービス内の intent.getStringExtra("calling_package")、intent.getStringExtra("sender")、callingPackage == "..."悪意のある呼び出し元がインテントの追加データや文字列を偽造します文字列による確認を削除し、Binder.getCallingUid() と pm.hasSigningCertificate() を使って呼び出し元 UID を暗号学的に認証します。
バインドされたサービスの権限バイパスBinder AIDL スタブ内の checkCallingOrSelfPermission() または enforceCallingOrSelfPermission()IPC の外部や ID がクリアされた場合にホストアプリの UID にフォールバックし、Confused Deputy によるバイパスが発生しますenforceCallingPermission("...WRITE_DATA", ...) または checkCallingPermission("...WRITE_DATA") に置き換えます。
バインドされたサービスでのメソッド単位の確認不足書き込み権限を強制せずに状態を変更する、バインドされたサービスの AIDL メソッド読み取り専用のバインドアクセス権を持つ呼び出し元が、状態を変更する書き込み操作を実行します状態を変更する AIDL メソッド内に enforceCallingPermission("...WRITE_DATA", "Caller lacks WRITE_DATA permission") を追加します。
不適切なランタイム権限フローshouldShowRequestPermissionRationale() の欠如、恒久的な拒否への対応不足、またはループ内での再要求ユーザー体験の低下、無限ループ、または権限を再取得できない問題が発生します3 状態のフローを実装します:権限の付与を確認 -> 説明ダイアログを確認 -> コントラクトを起動します。恒久的に拒否された場合は、Settings.ACTION_APPLICATION_DETAILS_SETTINGS にリダイレクトします。
位置情報の同時要求1 回の要求での 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() で動的に確認し、保護対象の呼び出しを SecurityException を捕捉する try-catch ブロックで囲みます。
安全でないブロードキャストパッケージの宛先や受信者の権限を指定しない 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+)

特定の外部パートナーアプリにアクセスを許可するには、マニフェストで <permission> を宣言し、信頼された SHA-256 証明書ダイジェストのリソース配列を android:knownCerts で参照して、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="/" は絶対に使用しないでください。
  • カスタム権限の宣言:プロバイダーで読み取り権限と書き込み権限を分ける場合、<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 を追加します。
    • アクセスを永続化する:受信側アプリは 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" />

次に、それらの権限と範囲を限定した 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. 暗号学的な呼び出し元 ID の検証

インテントまたは文字列引数で渡される呼び出し元のパッケージ名(getCallingPackage()、intent.getStringExtra("calling_package")、 intent.getStringExtra("sender")、intent.getStringExtra("package"))を決して信頼しないでください。インテントの追加情報や文字列形式のパッケージ名は、悪意のあるアプリによって偽造される可能性があります。

重大な脆弱性:インテントの追加情報を使ったパッケージ名のなりすまし
// 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() を使って呼び出し元の ID を取得します。
  3. PackageManager.hasSigningCertificate()(API 28 以降)または PackageManager.getPackagesForUid() を使って呼び出し元の証明書を検証します。
CallerVerifier の完全な実装

現在、偽装可能なチェックを実行しているサービスにこれを適用します。

  • 配置:修正するサービスと同じファイル内で CallerVerifier を宣言します(たとえば、SecureDataService.kt の内部)。検証を新しいファイルに移し、元のサービスをそのままにしないでください。
  • アクティブな Binder IPC コンテキスト:Binder.getCallingUid() がリモート呼び出し元の UID を返すのは、アクティブな Binder トランザクション中のみです(Process.myUid() を返す Service.onStartCommand() または Service.onBind() 内では決して返しません)。callingUid == Process.myUid() を拒否し、アクティブな IPC トランザクション外での呼び出しや Binder.clearCallingIdentity() の実行後の呼び出しでは、検証を回避するのではなく、デフォルトでアクセスを拒否するようにします。
  • Null 許容性: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 呼び出し元の ID が失われた場合、ホストアプリの権限をチェックするように切り替わります。ホストアプリはその権限を持っているため、この切り替えによってアクセスが許可され、セキュリティ境界が回避されて、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. 権限を要求しない代替手段と段階的なフロー

写真やメディアの選択を実装する際は、広範なストレージ権限の要求を避けるため、Photo Picker API guide の手順に従う必要があります。

  • Photo Picker:広範なストレージ権限(READ_MEDIA_IMAGES、READ_EXTERNAL_STORAGE)を要求するのではなく、ActivityResultContracts.PickVisualMedia() を優先します。
  • 段階的な位置情報フロー:まず ACCESS_FINE_LOCATION と ACCESS_COARSE_LOCATION を要求します。ACCESS_BACKGROUND_LOCATION を同時に要求してはいけません。バックグラウンド位置情報は、フォアグラウンド権限が許可された後、別の手順でのみ要求します。
Photo picker の実装
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() で動的に確認します。
  • API 呼び出しを try-catch で囲みます:権限で保護されたフレームワーク呼び出し(位置情報の取得など)の周囲で SecurityException を捕捉し、1 回限りの権限取り消しを適切に処理します。
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 以降の共有 ID)

明示的なインテント、必須のレシーバー権限、または 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 を取得したアプリであれば、誰でも追加の情報を付けて送信できるためです(Sender of Pending Intents を参照)。代わりに、マニフェストで signature または signature|knownSigner(API 31 以降)の権限を使って <receiver> を制限します。minSdkVersion が 31 未満の場合は、バインドされた Service または ContentProvider を使用し、Binder.getCallingUid() を検証します。


品質チェックリスト

実施事項
  • カスタム権限および ContentProvider の分割権限について、<manifest> のルートに <permission android:name="..." android:protectionLevel="signature" />(または signature|knownSigner)を必ず宣言します。
  • カスタム権限管理を不要にするため、すべての非公開コンポーネントで android:exported="false" を必ず明示的に設定します。
  • 偽装可能なインテントの追加情報を確認するのではなく、Binder.getCallingUid() と証明書ハッシュ(PackageManager.hasSigningCertificate または getPackagesForUid)を使用して、呼び出し元を暗号学的に必ず認証します。
  • 実行時の権限要求を行う前、および拒否への対応時に、shouldShowRequestPermissionRationale() を必ず確認します。
  • 権限が恒久的に拒否された場合は、ユーザーを Settings.ACTION_APPLICATION_DETAILS_SETTINGS に必ず誘導します。
  • 変更を伴う AIDL メソッドでは、enforceCallingPermission() または checkCallingPermission() を使用してメソッド単位のチェックを必ず適用します。
  • android:grantUriPermissions="true" を維持しつつ、ContentProvider の URI 権限を明示的なサブパス(例:android:pathPrefix="/shared/")に必ず限定します。
  • 保護された API 呼び出しの周囲で SecurityException を必ず捕捉します。
  • フォアグラウンドとバックグラウンドの位置情報権限は必ず段階的に要求し、同時に要求してはいけません。
禁止事項
  • 絶対に、Intent extras(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="" を宣言したり、残したりしないでください。必ずスコープを限定したサブパスに置き換えてください。
  • 絶対に、拒否された場合に無限の権限リクエストループを作成しないでください。
  • 絶対に、フォアグラウンド位置情報権限とバックグラウンド位置情報権限を同じリクエストで要求しないでください。
  • 絶対に、権限の許可状態をインメモリフィールドにキャッシュしないでください。

コメント