Skip to content

guard non-string payload fields in FIRMessagingContextManagerService - #16520

Open
isl-Ramzi wants to merge 4 commits into
firebase:mainfrom
isl-Ramzi:context-manager-payload-type-guard
Open

guard non-string payload fields in FIRMessagingContextManagerService#16520
isl-Ramzi wants to merge 4 commits into
firebase:mainfrom
isl-Ramzi:context-manager-payload-type-guard

Conversation

@isl-Ramzi

Copy link
Copy Markdown
Contributor

Repro: send a push whose google.c.cm.lt_start is a number rather than a date string. On receipt isContextManagerMessage: calls -length on that value and the app hits -[__NSCFNumber length]: unrecognized selector, so a single message crashes the receiver. The same holds for the body, title, sound, category and badge fields in contentFromContextualMessage:.
Cause: those fields are read straight from the untrusted push payload and assumed to be a specific type before a type-specific selector is sent to them.
Fix: check each field with isKindOfClass: before use, the way the rest of the module already guards payload dictionaries. Valid payloads behave the same, and a regression test covers a non-string start time and non-string content fields.

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

@paulb777

Copy link
Copy Markdown
Member

/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 introduces type-safety checks for fields extracted from untrusted push payloads in FIRMessagingContextManagerService to prevent crashes caused by unexpected data types, and adds corresponding unit tests. The review feedback correctly identifies additional locations in the same file where untrusted payload fields (such as kFIRMessagingContextManagerLocalTimeStart in handleContextManagerMessage: and kFIRMessagingContextManagerLocalTimeEnd) are still accessed without validation, presenting further opportunities for crashes that should be addressed.

Comment on lines +75 to +76
id startTime = message[kFIRMessagingContextManagerLocalTimeStart];
if (![startTime isKindOfClass:[NSString class]] || ![(NSString *)startTime length]) {

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.

high

While guarding kFIRMessagingContextManagerLocalTimeStart here is a great step, there are two other places in this file where untrusted payload fields can still cause crashes:

  1. handleContextManagerMessage:: This is a public method that directly accesses message[kFIRMessagingContextManagerLocalTimeStart] and calls .length on it without type validation. If called directly with a non-string start time, it will crash.

  2. kFIRMessagingContextManagerLocalTimeEnd: In handleContextManagerLocalTimeMessage:, the end time is read from the untrusted payload and passed directly to [dateFormatter dateFromString:] without type validation. If endTimeString is a number or other non-string type, it will crash.

Consider updating these methods as well to ensure complete safety against malformed payloads.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call, both were still exposed. Pushed a follow-up: handleContextManagerMessage: now type-checks the start time before -length, and handleContextManagerLocalTimeMessage: guards both the start and end times before handing them to dateFromString:. Added a regression test that runs a non-string start and a non-string end through the public handler.

@paulb777

Copy link
Copy Markdown
Member

/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 hardens the FIRMessagingContextManagerService against untrusted push payloads by adding type checks for fields like start/end times, badge, body, title, sound, and category, preventing potential crashes when unexpected JSON types are received. Unit tests have also been added to verify these safeguards. The review feedback highlights a critical crash risk in handleContextManagerLocalTimeMessage where a nil startDate (resulting from an invalid date string) could be passed to compare:, as well as a logic bug where endTimeString is checked instead of endDate after parsing.

@"Invalid local start date format %@. Message dropped", startTime);
return NO;
}
NSString *startTimeString = startTime;

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.

critical

The subsequent check if (!startTimeString) on line 112 is redundant because startTime is already guaranteed to be a non-nil NSString due to the isKindOfClass: check on line 106.\n\nMore importantly, there is a critical crash risk here: if startTimeString is an invalid date string (e.g., "invalid-date"), [dateFormatter dateFromString:startTimeString] on line 121 will return nil. Passing nil to [currentDate compare:] on line 124 will raise an NSInvalidArgumentException and crash the app because compare: does not accept nil.\n\nTo fix this, you should remove the redundant if (!startTimeString) check and instead add a check for if (!startDate) after it is parsed on line 121:\n\nobjc\n NSDate *startDate = [dateFormatter dateFromString:startTimeString];\n if (!startDate) {\n FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService002,\n @"Invalid local start date format %@. Message dropped",\n startTimeString);\n return NO;\n }\n

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Dropped the redundant startTimeString nil check and now bail out when dateFromString: returns nil, so the nil startDate never reaches compare:.

@"Invalid local end date format %@. Message dropped", endTime);
return NO;
}
NSString *endTimeString = endTime;

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.

high

The check on line 143 is if (!endTimeString), but it should be if (!endDate).\n\nSince endTimeString is already guaranteed to be a non-nil NSString (due to the checks on lines 129 and 135), if (!endTimeString) will always evaluate to false. If dateFormatter fails to parse the string (returning nil for endDate), the code will proceed with a nil endDate.\n\nWhile sending a message to nil in Objective-C does not crash (it returns 0/NSOrderedSame), it will cause a logic bug on line 149 where [endDate compare:currentDate] evaluates to NSOrderedSame, preventing the message from being dropped when it has an invalid end date format.\n\nPlease update the check on line 143 to verify endDate instead:\n\nobjc\n NSDate *endDate = [dateFormatter dateFromString:endTimeString];\n if (!endDate) {\n FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService004,\n @"Invalid local end date format %@. Message dropped", endTimeString);\n return NO;\n }\n

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, that check was dead. Switched it to test endDate, so an unparseable end time is dropped instead of falling through to compare: as NSOrderedSame.

@isl-Ramzi

Copy link
Copy Markdown
Contributor Author

Pushed the nil-date handling for both start and end, and extended the handler test to cover string times that don't parse.

@paulb777

Copy link
Copy Markdown
Member

/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 FIRMessagingContextManagerService by adding type and nil checks on fields extracted from untrusted push payloads (such as start/end times, badge, body, title, sound, and category) to prevent crashes from unexpected types. Unit tests have been added to verify these safety checks. The review feedback suggests optimizing performance by caching the NSDateFormatter instance using dispatch_once instead of instantiating it on every method call.

Comment on lines 112 to 114
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setDateFormat:kLocalTimeFormatString];

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.

medium

Creating an NSDateFormatter is an expensive operation. Since NSDateFormatter is thread-safe on iOS 7+ / macOS 10.9+, we can initialize it once using dispatch_once and reuse it to improve performance, especially since it is used multiple times within this method.

  static NSDateFormatter *dateFormatter;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
    [dateFormatter setDateFormat:kLocalTimeFormatString];
  });

@paulb777

Copy link
Copy Markdown
Member

PTAL at the CI issues

@isl-Ramzi

Copy link
Copy Markdown
Contributor Author

The CI failure was my test's fault, not the code. The non-string body/title fields get left unset, and the UNMutableNotificationContent default for those is nil on macOS/Catalyst here rather than an empty string, so the equal-to-@"" asserts failed. Switched those to check the field length is 0 instead, which holds either way. Pushed.

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