Loading...
Loading...
Audits, detects gaps, and remediates Android permissions and IPC component security vulnerabilities. Use when reviewing AndroidManifest.xml, defining custom permissions, securing Services, Receivers, and Providers, verifying caller signatures and UID using Binder, or implementing runtime permission flows.
npx skill4agent add android/skills android-permissions-security<permission>normaldangeroussignatureknownSignerBinder.getCallingUid()grantUriPermissionsandroidx.core:core-ktxandroidx.activity:activity-ktxandroidx.fragment:fragment-ktxbuild.gradleContextCompatActivityResultContractsknownSignerandroid:knownCertscompileSdktargetSdkBroadcastOptions.setShareIdentityEnabledcompileSdktargetSdk| Vulnerability Area | Search In / Pattern | Risk | Remediation Action |
|---|---|---|---|
| Weak Custom Permission Protection | | Unauthorized apps request or claim permission without restriction | Upgrade to |
| Overly Broad URI Permission Grants | | Leaks all provider data to third parties | Replace with explicit scoped subpath (e.g. |
| Missing ContentProvider Split Permissions | | Read-only callers execute write operations or write-only callers access read operations | Configure granular |
| Spoofable Caller Identity Checks | | Malicious callers forge intent extras or strings | Remove string checks; authenticate caller UID cryptographically using |
| Bound Service Permission Bypass | | Falls back to host app UID outside IPC or when identity is cleared, causing Confused Deputy bypass | Replace with |
| Missing Method-Level Checks in Bound Services | Bound Service AIDL methods mutating state without enforcing write permissions | Callers with read-only binding access invoke mutating write operations | Add |
| Flawed Runtime Permission Flow | Missing | Poor user experience, infinite loops, or inability to recover permissions | Implement 3-state flow: check grant -> check rationale dialog -> launch contract. On permanent denial, redirect to |
| Simultaneous Location Requests | | Rejected by Android 11+ (API 30+) | Request foreground location ( |
| Broad Storage Permissions for Media | Requesting | Over-privilege, user privacy violation | Migrate to zero-permission Photo Picker ( |
| Cached Permission State | Storing permission status in | Fails when permission is revoked or expires | Query |
| Insecure Broadcasts | | Data interception or command spoofing | Set explicit package ( |
<permission>signaturesignature|knownSignerandroid:knownCertsnormaldangeroussignatureOrSystemsignatureOrSystemsignaturesignature<permission>android:permission<permission
android:name="com.example.snippets.permission.ACCESS_SECURE_API"
android:protectionLevel="signature" />signature|knownSigner<permission>android:knownCerts<resources xmlns:tools="http://schemas.android.com/tools">
<!-- SHA-256 fingerprints of trusted partner certificates (lowercase, no colons) -->
<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" />android:exported="false"android:permissionandroid:readPermissionandroid:writePermissionandroid:grantUriPermissions="true"pathPrefix="/"<permission>android:protectionLevel="signature"signature|knownSigner<manifest><grant-uri-permission android:pathPrefix="/" />pathPrefix=""android:pathPrefix="/shared/"UriPermissionandroid.content.UriPermissioncontext.grantUriPermission(targetPackage, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)Intent.FLAG_GRANT_READ_URI_PERMISSIONcontentResolver.takePersistableUriPermission(uri, modeFlags)contentResolver.persistedUriPermissionsList<UriPermission>uriPermission.uriuriPermission.isReadPermission()uriPermission.isWritePermission()context.revokeUriPermission(uri, modeFlags)contentResolver.releasePersistableUriPermission(uri, modeFlags)<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
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>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
)
}
}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!
}intent.getStringExtra("calling_package")intent.getStringExtra("sender")callingPackage == "..."Binder.getCallingUid()PackageManager.hasSigningCertificate()PackageManager.getPackagesForUid()CallerVerifierSecureDataService.ktpackageInfo.signaturesArray<Signature>??: return falsepackageInfo.signaturesobject CallerVerifier {
private const val TRUSTED_PARTNER_SHA256 =
"A1B2C3D4E5F60708090A0B0C0D0E0F1011121314151617181920212223242526"
fun isCallerAuthorized(context: Context): Boolean {
val callingUid = Binder.getCallingUid()
if (callingUid == android.os.Process.myUid()) return true
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)
if (pm.hasSigningCertificate(callingUid, certBytes, PackageManager.CERT_INPUT_SHA256)) {
return true
}
}
// 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
}
}enforceCallingPermission()checkCallingOrSelfPermission()enforceCallingOrSelfPermission()enforceCallingPermission("com.example.snippets.permission.WRITE_DATA", "Caller lacks WRITE_DATA permission")checkCallingPermission("com.example.snippets.permission.WRITE_DATA")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.
this@SecureBoundService.enforceCallingPermission(
"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
}
}ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTEDshouldShowRequestPermissionRationale(permission)ActivityResultContracts.RequestPermission()!shouldShowRequestPermissionRationale(permission)Settings.ACTION_APPLICATION_DETAILS_SETTINGSclass 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)
}
}ActivityResultContracts.PickVisualMedia()READ_MEDIA_IMAGESREAD_EXTERNAL_STORAGEACCESS_FINE_LOCATIONACCESS_COARSE_LOCATIONACCESS_BACKGROUND_LOCATIONprivate 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()
}
}cachedLocationPermissionGrantedContextCompat.checkSelfPermission()SecurityExceptionfun 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)
}
}// 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) {}
}PendingIntentFLAG_IMMUTABLEpendingIntent.creatorPackage<permission android:name="..." android:protectionLevel="signature" />signature|knownSigner<manifest>android:exported="false"Binder.getCallingUid()PackageManager.hasSigningCertificategetPackagesForUidshouldShowRequestPermissionRationale()Settings.ACTION_APPLICATION_DETAILS_SETTINGSenforceCallingPermission()checkCallingPermission()android:pathPrefix="/shared/"android:grantUriPermissions="true"SecurityExceptionintent.getStringExtra("calling_package")intent.getStringExtra("sender")intent.getStringExtra("package")Binder.getCallingUid()checkCallingOrSelfPermission()enforceCallingOrSelfPermission()enforceCallingPermission()checkCallingPermission()signatureOrSystem<grant-uri-permission android:pathPrefix="/" />pathPrefix=""