fix: [SDK-3475] restrict notification component exports#2659
fix: [SDK-3475] restrict notification component exports#2659fadi-george wants to merge 2 commits into
Conversation
📊 Diff Coverage Report✓ Coverage check passed (no source files changed) |
|
@claude[agent] review |
|
@claude review |
There was a problem hiding this comment.
HMS regression from the prior revision is fixed; remaining nit on QUICKBOOT_POWERON is left as an inline comment for the author to consider.
Extended reasoning...
Overview
This is a manifest-only PR that flips six notification components in the notifications module from android:exported="true" to android:exported="false" to satisfy an Android security scanner finding. The single touched file is OneSignalSDK/onesignal/notifications/src/main/AndroidManifest.xml. The PR scope is narrow and well-described.
Security risks
The PR itself is a defensive hardening change (tightening component visibility). The risk profile is the inverse of typical PRs: the danger is not that the change introduces an exploit, but that it severs a legitimate cross-UID launch path. The prior revision did exactly that for NotificationOpenedActivityHMS, and was corrected in commit 22e4d2d to keep that activity exported. The remaining cross-UID concern is the QUICKBOOT_POWERON action on BootUpReceiver, which the bug hunter has flagged as a nit-level inline comment.
Level of scrutiny
Low–medium. Manifest-only, six lines changed, fully reversible, and the externally-launched HMS entry point has already been re-exported after my prior review. The pattern (security-scanner-driven export tightening) is well-understood and the other five components are safe (PendingIntent-driven or protected broadcasts).
Other factors
The author has demonstrated responsiveness — my previous critical finding on HMS was addressed in a follow-up commit. The remaining QUICKBOOT_POWERON finding is genuinely minor: it affects a small population of legacy HTC Sense / older Lenovo / older Samsung quick-power-on devices on API 21–25, only the quickboot fast-resume path (full-reboot restore via BOOT_COMPLETED is unaffected since it is a protected broadcast). Given the negligible 2026 impact and the trivial nature of the rest of the diff, this is approvable; the author can decide whether to drop the now-unreachable QUICKBOOT_POWERON filter action as documented cleanup.
| android:name="com.onesignal.notifications.receivers.BootUpReceiver" | ||
| android:exported="true"> | ||
| android:exported="false"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.BOOT_COMPLETED" /> | ||
| <action android:name="android.intent.action.QUICKBOOT_POWERON" /> | ||
| </intent-filter> | ||
| </receiver> |
There was a problem hiding this comment.
🟡 🟡 Setting BootUpReceiver to android:exported="false" silently disables the android.intent.action.QUICKBOOT_POWERON action that remains in its intent-filter. Unlike BOOT_COMPLETED (a protected broadcast sent by system_server under SYSTEM_UID, which still reaches non-exported receivers), QUICKBOOT_POWERON is not in AOSP's protected broadcast list — it is emitted by OEM launchers (HTC Sense, older Lenovo / older Samsung quick-power-on flows) under their own UIDs, so the platform will block cross-UID delivery to a non-exported receiver. Full-reboot restore via BOOT_COMPLETED is unaffected, only the quickboot fast-resume path on those legacy OEM devices regresses; fix by dropping the now-unreachable QUICKBOOT_POWERON action from the filter, splitting it into its own exported receiver, or keeping the receiver exported="true" with android:permission="android.permission.RECEIVE_BOOT_COMPLETED" as a guard. Nit — affected device population is very small in 2026.
Extended reasoning...
What the bug is. BootUpReceiver declares two actions in its intent-filter: android.intent.action.BOOT_COMPLETED and android.intent.action.QUICKBOOT_POWERON (manifest lines 84-90). The PR flips the receiver from exported="true" to exported="false". With exported="false", Android only delivers broadcasts from the same UID or from system_server for protected broadcasts. BOOT_COMPLETED IS in AOSP's protected broadcast list (declared in frameworks/base/core/res/AndroidManifest.xml) and is dispatched by system_server, so it continues to reach the receiver. QUICKBOOT_POWERON is NOT in that list — it is an OEM-specific broadcast originally introduced by HTC Sense's quickboot flow and adopted by some older Lenovo / older Samsung quick-power-on launchers, dispatched by an OEM launcher process under its own UID.
The code path that triggers it. BootUpReceiver.kt runs the same logic (NotificationRestoreWorkManager.beginEnqueueingWork) regardless of which action arrived — i.e., the SDK explicitly opted into supporting the quickboot fast-resume path when it added QUICKBOOT_POWERON to the filter. After this PR, an OEM launcher's QUICKBOOT_POWERON broadcast targeted at the OneSignal app's process is cross-UID, the receiver is non-exported, and Android's broadcast access control blocks the delivery. The action remains in the filter but is silently unreachable.
Why existing code doesn't prevent it. The other components that were also flipped to exported="false" in this PR are safe for different reasons: UpgradeReceiver receives MY_PACKAGE_REPLACED from system_server (protected), NotificationDismissReceiver and the two NotificationOpenedActivity variants are triggered via PendingIntent that runs under the host app's own UID. BootUpReceiver for BOOT_COMPLETED is similarly safe (protected broadcast from system_server). QUICKBOOT_POWERON is the one action with no such rescue: no protected-broadcast exemption, no same-UID sender, no PendingIntent wrapping.
Impact. On the affected legacy OEM devices (mostly older HTC Sense, some older Lenovo, some older Samsung quick-power-on), users who quickboot the device will no longer have OneSignal notifications restored via NotificationRestoreWorkManager. Full reboots still work because BOOT_COMPLETED is unaffected. This is exactly the scenario the PR description's 'Recommended follow-up coverage' lists as untested ('boot restore'). Impact in 2026 is narrow — HTC stopped selling Android phones around 2017, quickboot is largely deprecated, and Android 8+ also tightens manifest receivers for implicit broadcasts not on the exempt list — so the quickboot regression only meaningfully affects a small legacy population on API 21-25 devices that still emit QUICKBOOT_POWERON from a non-system-privileged sender.
How to fix. Pick one: (a) drop <action android:name="android.intent.action.QUICKBOOT_POWERON" /> from the intent-filter as a deliberate, documented behavior change since it is now unreachable anyway; (b) split QUICKBOOT_POWERON into its own <receiver android:exported="true"> so the cross-UID OEM-launcher broadcast can reach it; or (c) revert BootUpReceiver to android:exported="true" and add android:permission="android.permission.RECEIVE_BOOT_COMPLETED" as a guard (though some QUICKBOOT_POWERON senders may not hold that permission). The other five components in this PR can stay non-exported.
Step-by-step proof.
- App with OneSignal SDK is installed on an older HTC Sense device (API 21-25), and OneSignal has previously displayed notifications that are pending restore.
- User triggers quickboot (fast-resume) rather than a full reboot.
- HTC Sense's quickboot launcher process (its own UID, not SYSTEM_UID) calls
sendBroadcast(new Intent("android.intent.action.QUICKBOOT_POWERON")). - ActivityManagerService walks registered receivers for that action and finds
com.onesignal.notifications.receivers.BootUpReceiverdeclared withandroid:exported="false". QUICKBOOT_POWERONis not in AOSP's protected broadcast list, so AMS does NOT apply the SYSTEM_UID-bypass that lets protected broadcasts reach non-exported receivers.- AMS checks the sender UID (HTC launcher) against the receiver's component visibility: cross-UID broadcast to non-exported receiver → denied.
BootUpReceiver.onReceiveis never invoked forQUICKBOOT_POWERON.NotificationRestoreWorkManager.beginEnqueueingWorkis not enqueued on the quickboot path. Pending OneSignal notifications are not restored.- Confirm
BOOT_COMPLETEDstill works: on a full reboot,system_server(SYSTEM_UID) dispatches the protectedBOOT_COMPLETEDbroadcast; AMS allows it to reach non-exported receivers;BootUpReceiver.onReceivefires; restore proceeds normally. Only the quickboot fast-resume path is regressed.
Description
One Line Summary
Restrict OneSignal notification activities and receivers that do not need external app access to non-exported Android components.
Details
Motivation
Android security scanners flag several OneSignal notification components as exported without a guarding permission. These components are invoked by the SDK itself or by system broadcasts, so they should not be externally launchable by arbitrary apps.
Scope
This updates the notifications module manifest only.
FCMBroadcastReceiverremains exported because it is the FCM entry point and remains protected bycom.google.android.c2dm.permission.SEND.Testing
Unit testing
No new unit tests were added because this is a manifest-only component exposure change.
Manual testing
Automated verification run locally:
./gradlew :OneSignal:notifications:spotlessXmlCheck :OneSignal:notifications:processDebugManifest :OneSignal:notifications:processReleaseManifest :OneSignal:notifications:assembleReleaseonesignal/notifications/build/outputs/aar/notifications-release.aarand confirmed the six affected components are packaged withandroid:exported="false"whileFCMBroadcastReceiverremains exported withcom.google.android.c2dm.permission.SEND../gradlew :OneSignal:notifications:testDebugUnitTestDevice smoke testing has not been run yet. Recommended follow-up coverage: notification open, action button open, dismiss handling, boot restore, package upgrade restore, and HMS notification open.
Affected code checklist
Checklist
Overview
Testing
Final pass
Made with Cursor