What `adb` Can Teach You About Broken Android IPC Protections
When Android developers hear “permissions,” they usually think about camera prompts, location access, notifications, or storage.
Those matter, but they are not where some of the more interesting Android security mistakes appear. The more useful test often starts when another process tries to talk to your app.
Once you expose a `Service`, `ContentProvider`, or `BroadcastReceiver`, the question changes. It is no longer just “did we request the right permission?” It becomes “who is calling us, and what are we willing to trust about that caller?”
That is why the runtime permissions and IPC lab in the Android Security Training project is useful. The secure and vulnerable variants are close enough to compare directly, but different enough to show how quickly exported components become liabilities when they are left open or protected loosely.
Package Names Are Not Identity
The secure helper surfaces signer information:
The vulnerable helper is much thinner:
That difference matters because Android trust decisions should not be built on package names alone. A package name is useful for routing and display. It is not proof that the caller is the app you meant to trust.
For same-signer relationships, the signing certificate is the meaningful identity signal. The secure build computes and displays the signer digest alongside process identity information. The vulnerable build mostly stops at basic PID and UID details.
That contrast is useful in a teaching app because it pushes the reader toward the right question: not “what did the caller say its name was?” but “what identity did the platform actually assign to this caller?”
Exported Services and Providers Should Be Intentional
The provider query demo shows the difference clearly:
The service-start path follows the same theme:
The vulnerable build is deliberately permissive. If a component is exported without meaningful protection, another app, or the shell user during testing, can often interact with it.
The secure build is stricter. Components that do not need external access are kept private. Components that are intentionally shared are guarded by a custom signature permission, so only callers signed with the expected key should be allowed through.
That may sound like ceremony until you test it:
adb shell am startservice -n dev.jamescullimore.android_security_training.vuln/dev.jamescullimore.android_security_training.perm.DemoService
adb shell content query - uri content://dev.jamescullimore.android_security_training.vuln.demo/hello
On the vulnerable side, those calls are expected to succeed in the lab. On the secure side, the same style of access should be blocked by export settings, permissions, or both.
ADB Is a Good Reality Check
One reason I like this lab is that you do not need to write a second app to start testing your assumptions. Shell commands are enough to reveal whether the merged manifest matches what you thought you built.
Get James Cullimore’s stories in your inbox
Join Medium for free to get updates from this writer.
Start by finding the installed packages:
adb shell pm list packages | grep android_security_training
Then inspect the packaged app:
adb shell dumpsys package dev.jamescullimore.android_security_training.secure | sed -n '1,180p'
adb shell dumpsys package dev.jamescullimore.android_security_training.vuln | sed -n '1,180p'
That is where vague confidence gets cleaned up. Source manifests, library manifests, product flavors, and manifest merging can all change the final shape of the app. If you only review the source fragment, you are reviewing an intention. The installed package is the thing users actually run.
The Merged Manifest Is the Source of Truth
This is where Android reviews can surprise people.
The manifest you wrote is only one input. Libraries can contribute components. Product flavors can override attributes. Build variants can change package names, permissions, exported flags, authorities, and intent filters. A component that looked private in one source file may not be private in the artifact you installed.
That is why I like checking the final package from more than one angle. Use Android Studio’s merged manifest view during development, then verify the installed app with `dumpsys package`. If a provider authority, exported receiver, or service permission differs between secure and vulnerable builds, that difference should be visible in the final artifact.
This also helps catch accidental drift. A signature permission is useful only if the component actually requires it. A non-exported service is useful only if it is still non-exported after merging. A provider authority is useful only if it is not predictable in a way that collides across variants or apps.
For release reviews, I would also compare secure and vulnerable flavors explicitly. Differences in permissions, authorities, and exported attributes should be deliberate. If a debug-only component appears in the release package, the review should stop until someone can explain why it ships.
Caller Checks Should Match the Sensitivity of the Operation
Not every IPC path needs the same level of ceremony.
A harmless diagnostic provider that returns static demo text does not carry the same risk as a service that changes account state, exports files, or starts a privileged workflow. The review should match the operation.
For low-risk paths, keeping the component private may be enough. For same-suite communication, signature permissions are often a better fit than runtime permissions. For sensitive Binder or provider operations, the callee may need to inspect the caller UID, resolve packages for that UID, and compare signer identity before doing anything irreversible.
The key is to make the trust decision explicit. If the app is about to reveal data or perform an action on behalf of another process, there should be a clear reason that caller is allowed to ask.
Good IPC Hygiene Is Boring for a Reason
The secure version is not doing anything exotic. It applies platform rules consistently:
- keep components non-exported unless external access is required
- use signature permissions for same-signer IPC
- inspect the merged manifest and installed package
- check caller identity when the trust boundary matters
- avoid treating package names as proof
That last point is worth repeating because it is a common shortcut. If a provider, service, or receiver makes a sensitive decision, the caller identity should come from platform-enforced properties, not from a string that merely looks familiar.
Closing Thoughts
The permissions lab is useful because it moves the conversation away from prompts and into process boundaries.
The secure build treats IPC as a trust problem. The vulnerable build treats it as plumbing. That is the real contrast.
If you want tighter Android boundaries, do the boring work carefully: keep components private by default, protect intentional sharing with signature permissions, verify identity when needed, and inspect the final packaged app. Most IPC bugs are not exotic. They come from leaving a component reachable and assuming nobody will try to call it.