Skip to content

fix: [SDK-3475] restrict notification component exports#2659

Open
fadi-george wants to merge 2 commits into
mainfrom
fadi/sdk-3475
Open

fix: [SDK-3475] restrict notification component exports#2659
fadi-george wants to merge 2 commits into
mainfrom
fadi/sdk-3475

Conversation

@fadi-george
Copy link
Copy Markdown
Contributor

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. FCMBroadcastReceiver remains exported because it is the FCM entry point and remains protected by com.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:assembleRelease
  • Inspected onesignal/notifications/build/outputs/aar/notifications-release.aar and confirmed the six affected components are packaged with android:exported="false" while FCMBroadcastReceiver remains exported with com.google.android.c2dm.permission.SEND.
  • ./gradlew :OneSignal:notifications:testDebugUnitTest

Device 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

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
    • If it is hard to explain how any codes changes are related to each other then it most likely needs to be more than one PR
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
    • Simplify with less code, followed by splitting up code into well named functions and variables, followed by adding comments to the code.
  • I have reviewed this PR myself, ensuring it meets each checklist item
    • WIP (Work In Progress) is ok, but explain what is still in progress and what you would like feedback on. Start the PR title with "WIP" to indicate this.

Made with Cursor

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

📊 Diff Coverage Report

✓ Coverage check passed (no source files changed)

📥 View workflow run

@fadi-george
Copy link
Copy Markdown
Contributor Author

@claude[agent] review

@fadi-george
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread OneSignalSDK/onesignal/notifications/src/main/AndroidManifest.xml
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 84 to 90
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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 🟡 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.

  1. 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.
  2. User triggers quickboot (fast-resume) rather than a full reboot.
  3. HTC Sense's quickboot launcher process (its own UID, not SYSTEM_UID) calls sendBroadcast(new Intent("android.intent.action.QUICKBOOT_POWERON")).
  4. ActivityManagerService walks registered receivers for that action and finds com.onesignal.notifications.receivers.BootUpReceiver declared with android:exported="false".
  5. QUICKBOOT_POWERON is not in AOSP's protected broadcast list, so AMS does NOT apply the SYSTEM_UID-bypass that lets protected broadcasts reach non-exported receivers.
  6. AMS checks the sender UID (HTC launcher) against the receiver's component visibility: cross-UID broadcast to non-exported receiver → denied.
  7. BootUpReceiver.onReceive is never invoked for QUICKBOOT_POWERON.
  8. NotificationRestoreWorkManager.beginEnqueueingWork is not enqueued on the quickboot path. Pending OneSignal notifications are not restored.
  9. Confirm BOOT_COMPLETED still works: on a full reboot, system_server (SYSTEM_UID) dispatches the protected BOOT_COMPLETED broadcast; AMS allows it to reach non-exported receivers; BootUpReceiver.onReceive fires; restore proceeds normally. Only the quickboot fast-resume path is regressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant