Skip to content

AB#292839 fix(push): stop force-casting the notification message payload - #149

Open
eligutovsky wants to merge 1 commit into
masterfrom
fix/292839-push-payload-force-casts
Open

AB#292839 fix(push): stop force-casting the notification message payload#149
eligutovsky wants to merge 1 commit into
masterfrom
fix/292839-push-payload-force-casts

Conversation

@eligutovsky

@eligutovsky eligutovsky commented Aug 5, 2026

Copy link
Copy Markdown
Member

AB#292839

Description of Changes

PushNotification.init(userInfo:) force-cast two values it had not checked:

let msgData = msg["data"] as! [AnyHashable: Any]
id = msgData["id"] as! Int

A payload carrying custom.a.k.message without a well-formed data.id took the host app down.
This initialiser runs on every notification the app receives, ours or not — both
UNUserNotificationCenter delegate methods build a PushNotification before deciding whose
notification it is — so a malformed payload from any sender was enough.

Both casts are now checked. Leaving id at 0 marks the notification as not ours, which is
already how the delegate decides to forward it to the host app's delegate, so a payload we
cannot read is treated the same as any other notification that is not ours.

Verification

The crash reproduces. Running the new tests against master gives four distinct crashes,
one per malformed shape, at exactly the two lines above:

Could not cast value of type 'Swift.String' to 'Swift.Dictionary<Swift.AnyHashable, Any>'
Could not cast value of type 'Swift.String' to 'Swift.Int'
OptimoveSDK/Optimobile+Push.swift:47: Fatal error: Unexpectedly found nil while unwrapping an Optional value
OptimoveSDK/Optimobile+Push.swift:49: Fatal error: Unexpectedly found nil while unwrapping an Optional value

Each one kills the test process, which is what the crash does to a host app. With the fix, the
same 11 tests pass and nothing crashes.

Full suite: 86 tests, 0 failures (75 before, plus the 11 added here).

The 11 tests cover the well-formed payload (message id, deep link URL, aps/data passthrough),
the payloads that were already handled (no payload, no aps, no custom, no k.message), and
the four that crashed (no data, data not a dictionary, no id, id not a number).

One consequence worth naming

A malformed id used to crash loudly. It now logs a warning and the notification is treated as
not ours. If the push service ever changed the id's type, the old behaviour would have produced
a crash report, whereas this will silently stop attributing opens. That is why both branches log
rather than return quietly — the log line is what makes it diagnosable:

Ignoring a notification whose k.message carries no data dictionary
Ignoring a notification whose k.message data carries no numeric id

Accepting a string id as well would paper over exactly that kind of change, so the check stays
strict.

Breaking Changes

  • None

No public API change. The only behaviour change is in cases that previously crashed.

Release Checklist

Prepare:

  • Detail any breaking changes. Breaking changes require a new major version number — none
  • Check pod lib lint passes
  • Update any relevant sections of the repository wiki pages on a branch — n/a, no public API change

Bump versions in:

Bumped to 6.8.4, the next number not already claimed by an open PR (6.8.1 → #144,
6.8.2 → #147, 6.8.3 → #148). This is not sustainable: with several PRs open at once, a
per-PR bump means whichever merges first is right and the rest have to be renumbered. Worth
deciding to bump once at release time instead, and dropping the version rows from the per-PR
checklist.

  • OptimoveCore.podspec
  • OptimoveNotificationServiceExtension.podspec
  • OptimoveSDK.podspec
  • OptimoveCore/Sources/Classes/Constants/SDKVersion.swift
  • README.md — n/a, contains no version reference
  • CHANGELOG.md
  • Update major version numbers in wiki (basic integration + push guides) — n/a, patch release

Integration tests

Unit tests pass (86, 0 failures). Parsing is fully covered by them; what needs a device is only
that a normal Optimove push is unaffected:

  • Init SDK with all credentials
  • Register for push
  • Send test push, verify it opens and the open is attributed
  • Receive / trigger deep link handler (In-App/Push)

Release:

  • Squash and merge to master
  • Delete branch once merged

AB#292839

PushNotification.init(userInfo:) force-cast msg["data"] and its "id".
A payload carrying custom.a.k.message without a well-formed data.id took
the host app down, and this initialiser runs on every notification the app
receives, ours or not, so a malformed payload from any sender was enough.

Both casts are now checked. Leaving id at 0 marks the notification as not
ours, which is already how the delegate decides to forward it to the host
app, so an unreadable payload is treated like any other notification that
is not ours. Both branches log, because a malformed id used to produce a
crash report and would otherwise now fail silently.

Verified by running the new tests against master first: four distinct
crashes at the two lines, one per malformed shape.
Copilot AI review requested due to automatic review settings August 5, 2026 13:23
@eligutovsky eligutovsky changed the title fix(push): stop force-casting the notification message payload AB#292839 fix(push): stop force-casting the notification message payload Aug 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens PushNotification.init(userInfo:) so malformed notification payloads can no longer crash the host app during push parsing, and adds unit tests to lock in the new non-crashing behavior. It also bumps the SDK patch version and documents the fix in the changelog.

Changes:

  • Replace two unsafe force-casts in push payload parsing with guarded casts plus warning logs, treating unreadable payloads as “not ours” (id == 0).
  • Add a dedicated unit test suite covering well-formed, foreign, and previously-crashing malformed payload shapes.
  • Bump SDK version to 6.8.4 across podspecs / SDKVersion.swift and add a CHANGELOG entry.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift Removes force-casts in PushNotification.init(userInfo:), adds guarded parsing with warnings and preserves id == 0 behavior for non-Optimove/malformed payloads.
OptimoveSDK/Tests/Sources/Optimobile/PushNotificationTests.swift Adds comprehensive tests for valid/invalid payload shapes, including the previously crashing cases.
OptimoveSDK.podspec Bumps pod version to 6.8.4.
OptimoveNotificationServiceExtension.podspec Bumps pod version to 6.8.4.
OptimoveCore.podspec Bumps pod version to 6.8.4.
OptimoveCore/Sources/Classes/Constants/SDKVersion.swift Updates SDKVersion constant to 6.8.4.
CHANGELOG.md Adds 6.8.4 entry describing the crash fix and new behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

2 participants