Skip to content

fix(inappmessaging): harden parsing of server-provided campaign data - #16758

Open
paulb777 wants to merge 3 commits into
mainfrom
pb-harden-fiam-fetch-parser
Open

paulb777 wants to merge 3 commits into
mainfrom
pb-harden-fiam-fetch-parser

Conversation

@paulb777

@paulb777 paulb777 commented Oct 1, 2026

Copy link
Copy Markdown
Member

Context: #16728 — a malformed server payload was parsed into a nil dictionary key
and crashed apps on every launch. This PR audits FirebaseInAppMessaging's
handling of server-delivered campaign data (fetch responses, the on-disk
campaign cache re-read at launch, persisted impressions, and Clearcut
responses) for the same class of bug.

Most per-message parsing in FIRIAMFetchResponseParser is already wrapped in
@try/@catch, but that only covers exceptions thrown during parsing. Values
of the wrong type were stored as-is and crashed later, outside the @try.

Hardened code paths

  • FIRIAMFetchResponseParser
    • Top-level response that is not a dictionary (e.g. a JSON array) and a
      messages node that is not an array: these hit -objectForKeyedSubscript:
      / fast enumeration outside the @try and threw. The parser now returns nil,
      and callers already treat that as a failed fetch or an empty cache.
    • Message entries that are not dictionaries are now counted as discarded.
    • Analytics trigger event names must be non-empty strings. Before this change,
      {"name": 7} or null was stored. It was then added to the watch set and
      later sent -isEqualToString: from
      -[FIRIAMMessageDefinition messageRenderedOnAnalyticsEvent:], which crashes
      when a matching Analytics event fires. Because the response is cached on
      disk, the bad trigger came back on every launch.
    • campaignId (now must be a non-empty string), campaignName, title, body,
      button text, image and action URL strings, and fiamTrigger are now
      type-checked. Wrong-typed optional fields are dropped instead of being
      stored as NSNumber/NSArray in NSString properties that UI and
      bookkeeping code use later. experimentPayload is only passed to
      ABTExperimentPayload when it is a dictionary. Trigger entries that are not
      dictionaries are skipped.
  • UIColor+FIRIAMHexString: a non-string hex color returns nil, so the
    default color is used.
  • FIRIAMMsgFetcherUsingRestful: a 200 response with a nil body no longer
    passes nil to NSJSONSerialization, which throws NSInvalidArgumentException.
  • FIRIAMMessageClientCache: a nil analytics event name is never inserted
    into firebaseAnalyticEventsToWatch.
  • FIRIAMBookKeeperViaUserDefaults: malformed persisted impression entries
    are now skipped. Before this change, getImpressions added nil to an array
    (initWithStorageDictionary: returns nil for bad entries), and both
    getImpressions and getMessageIDsFromImpressions subscripted non-dictionary
    entries. getImpressions runs on every fetch check, so corrupt entries
    crashed the app repeatedly.
  • FIRIAMClearcutHttpRequestSender: now handles a nil body, a JSON body that
    is not a dictionary, and a next_request_wait_millis that is not a
    number/string (e.g. null, [], {}). Each of these used to throw.
    Wait-time range clamping already exists in FIRIAMClearcutUploader.

Valid payloads parse the same as before. The existing fixtures and assertions
are unchanged.

Tests

New fixture: JsonDataWithMalformedMessagesFromFetch.txt. New/extended tests
in FIRIAMFetchResponseParserTests, FIRIAMMessageClientCacheTests,
FIRIAMBookKeeperViaUserDefaultsTests, FIRIAMMsgFetcherUsingRestfulTests, and
a new FIRIAMClearcutHttpRequestSenderTests.

These 12 tests fail on main (with the source changes reverted):

  • testParsingMalformedMessageNodes: wrong-typed ids, names, titles, and event
    names were accepted
  • testParsingNonDictionaryResponse, testParsingNonArrayMessagesNode:
    unrecognized selector exceptions
  • testLoadingCachedResponseWithMalformedEventNames:
    -[NSConstantIntegerNumber isEqualToString:] from
    messageRenderedOnAnalyticsEvent:
  • testLoadingCorruptCachedResponse
  • testReadingMalformedImpressionRecords,
    testRecordingImpressionWithMalformedImpressionRecords
  • testFetchWithNilResponseBody, testFetchWithNonDictionaryJSONResponseBody,
    testFetchWithNonArrayMessagesInResponseBody
  • testMalformedResponseBodies, testNilResponseBody (Clearcut)

A few more tests (empty response, invalid expiration times, garbage JSON body,
non-array impressions, valid Clearcut wait times) already pass on main and
are added as regression coverage.

Testing

pod gen FirebaseInAppMessaging.podspec --platforms=ios, then ran
FirebaseInAppMessaging-Unit-unit on an iOS 26 simulator. The full unit suite
passes: 138 tests, 0 failures (121 before this change plus 17 new).

Validate types of values from campaign fetch responses, the on-disk campaign
cache, persisted impressions and Clearcut responses instead of storing
wrongly typed values that crash later outside the parser's @Try, e.g. a
non-string analytics event name crashing in -isEqualToString: when the event
fires, or nil being inserted into an array when reading impressions.

Refs #16728
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@danger-firebase-ios danger-firebase-ios Bot added the api: inappmessaging Firebase In App Messaging label Oct 1, 2026
@paulb777

paulb777 commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request significantly improves the robustness of Firebase In-App Messaging by adding strict type checking and validation when parsing API fetch responses, cached campaigns, and persisted impressions. It introduces helper functions and type checks across multiple classes to prevent crashes from malformed or unexpected JSON structures, accompanied by comprehensive unit tests. The review feedback suggests further enhancing safety by introducing a FIRIAMDictionaryOrNil helper function to safely parse nested dictionary structures and avoid unrecognized selector exceptions.

Comment thread FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m
Comment thread FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m Outdated
The parser returns nil for malformed top-level responses, which the static analyzer flagged as nullability.NullReturnedFromNonnull. Declare the return as nullable and treat a nil result from the cached response as an empty message set.
Address review feedback: add FIRIAMDictionaryOrNil and use it for every nested content lookup (title, body, action, buttons) across banner, modal, image-only, and card messages. A wrong-typed intermediate node now drops only the fields beneath it instead of raising an unrecognized selector exception that discarded the whole message.
@paulb777

paulb777 commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request improves the robustness of Firebase In-App Messaging when parsing malformed campaign fetch responses, cached campaigns, and persisted impressions. It introduces defensive type-checking helpers and validation checks across several components—including the response parser, HTTP request sender, bookkeeper, and client cache—to prevent crashes from unexpected JSON types. Additionally, comprehensive unit tests have been added to verify these robustness improvements. There are no review comments to address, and I have no feedback to provide.

@paulb777 paulb777 added this to the 13.1.0 - M188 milestone Oct 1, 2026

This branch has not been deployed

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

Labels

api: inappmessaging Firebase In App Messaging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants