Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Chrome for iOS
DescriptionInsufficient validation of untrusted input in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker505144022
Fix commit3976724788f0 (chromium/src) +44/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm
modified
if
ios/chrome/browser/share_extension/model/share_extension_utils.mm
modified

Files Changed

  • ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm
  • ios/chrome/browser/share_extension/model/share_extension_utils.mm
  • ios/chrome/common/ntp_tile/ntp_tile.h
  • ios/chrome/common/ntp_tile/ntp_tile.mm
  • ios/chrome/common/ui/favicon/favicon_attributes.h
  • ios/chrome/common/ui/favicon/favicon_attributes.mm
From 3976724788f092cfb41c0265101c1ae2cfa6e2b8 Mon Sep 17 00:00:00 2001
From: Olivier ROBIN <[email protected]>
Date: Wed, 29 Apr 2026 03:03:16 -0700
Subject: [PATCH] Use SecureCoding for app group serialized data

Use secure coding on extensions <-> chrome communication to avoid
unserializing uncontrolled data.

Bug: 505144022
Change-Id: Icac4fd5300646443b1beb1a46e95aad520494e5f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7800139
Commit-Queue: Olivier Robin <[email protected]>
Reviewed-by: Gauthier Ambard <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1622317}
---

diff --git a/ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm b/ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm
index e52f4d1..145d7efe 100644
--- a/ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm
+++ b/ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm
@@ -122,8 +122,11 @@
     return [[NSMutableDictionary alloc] init];
   }
 
-  unarchiver.requiresSecureCoding = NO;
-  return [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
+  unarchiver.requiresSecureCoding = YES;
+  NSSet* classes = [NSSet
+      setWithObjects:[NSDictionary class], [NSURL class], [NTPTile class], nil];
+  return [unarchiver decodeObjectOfClasses:classes
+                                    forKey:NSKeyedArchiveRootObjectKey];
 }
 
 void GetFaviconsAndSave(
@@ -222,7 +225,7 @@
   NSDate* last_modification_date = NSDate.date;
   NSError* error = nil;
   NSData* data = [NSKeyedArchiver archivedDataWithRootObject:most_visited_data
-                                       requiringSecureCoding:NO
+                                       requiringSecureCoding:YES
                                                        error:&error];
   if (!data || error) {
     DLOG(WARNING) << "Error serializing most visited: "
diff --git a/ios/chrome/browser/share_extension/model/share_extension_utils.mm b/ios/chrome/browser/share_extension/model/share_extension_utils.mm
index 06548a61..1cd09757 100644
--- a/ios/chrome/browser/share_extension/model/share_extension_utils.mm
+++ b/ios/chrome/browser/share_extension/model/share_extension_utils.mm
@@ -73,8 +73,12 @@
     return result;
   }
 
-  unarchiver.requiresSecureCoding = NO;
-  id entryID = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
+  unarchiver.requiresSecureCoding = YES;
+  NSSet* classes = [NSSet setWithObjects:[NSDictionary class], [NSNumber class],
+                                         [NSURL class], [NSString class],
+                                         [NSDate class], nil];
+  id entryID = [unarchiver decodeObjectOfClasses:classes
+                                          forKey:NSKeyedArchiveRootObjectKey];
   NSDictionary* entry = base::apple::ObjCCast<NSDictionary>(entryID);
   if (!entry) {
     return result;
diff --git a/ios/chrome/common/ntp_tile/ntp_tile.h b/ios/chrome/common/ntp_tile/ntp_tile.h
index 956cf46..bc7bde6 100644
--- a/ios/chrome/common/ntp_tile/ntp_tile.h
+++ b/ios/chrome/common/ntp_tile/ntp_tile.h
@@ -9,7 +9,7 @@
 
 // This class stores all the data associated with an NTP Most Visited tile
 // suggestion in an NSCoding-enabled format.
-@interface NTPTile : NSObject <NSCoding>
+@interface NTPTile : NSObject <NSSecureCoding>
 
 // The most visited site's title.
 @property(readonly, atomic) NSString* title;
diff --git a/ios/chrome/common/ntp_tile/ntp_tile.mm b/ios/chrome/common/ntp_tile/ntp_tile.mm
index e90d8a6..a11aa98f 100644
--- a/ios/chrome/common/ntp_tile/ntp_tile.mm
+++ b/ios/chrome/common/ntp_tile/ntp_tile.mm
@@ -61,18 +61,29 @@
   return self;
 }
 
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
-  return [self initWithTitle:[aDecoder decodeObjectForKey:kTitleKey]
-                          URL:[aDecoder decodeObjectForKey:kURLKey]
-              faviconFileName:[aDecoder decodeObjectForKey:kfaviconFileNameKey]
+  return [self initWithTitle:[aDecoder decodeObjectOfClass:[NSString class]
+                                                    forKey:kTitleKey]
+                          URL:[aDecoder decodeObjectOfClass:[NSURL class]
+                                                     forKey:kURLKey]
+              faviconFileName:[aDecoder decodeObjectOfClass:[NSString class]
+                                                     forKey:kfaviconFileNameKey]
             fallbackTextColor:[aDecoder
-                                  decodeObjectForKey:kFallbackTextColorKey]
+                                  decodeObjectOfClass:[UIColor class]
+                                               forKey:kFallbackTextColorKey]
       fallbackBackgroundColor:
-          [aDecoder decodeObjectForKey:kFallbackBackgroundColorKey]
+          [aDecoder decodeObjectOfClass:[UIColor class]
+                                 forKey:kFallbackBackgroundColorKey]
        fallbackIsDefaultColor:[aDecoder
                                   decodeBoolForKey:kFallbackIsDefaultColorKey]
-             fallbackMonogram:[aDecoder decodeObjectForKey:kFallbackMonogram]
-                     position:[[aDecoder decodeObjectForKey:kPosition]
+             fallbackMonogram:[aDecoder decodeObjectOfClass:[NSString class]
+                                                     forKey:kFallbackMonogram]
+                     position:[[aDecoder decodeObjectOfClass:[NSNumber class]
+                                                      forKey:kPosition]
                                   unsignedIntegerValue]];
 }
 
diff --git a/ios/chrome/common/ui/favicon/favicon_attributes.h b/ios/chrome/common/ui/favicon/favicon_attributes.h
index 0e9f38c..d2eb01b 100644
--- a/ios/chrome/common/ui/favicon/favicon_attributes.h
+++ b/ios/chrome/common/ui/favicon/favicon_attributes.h
@@ -12,7 +12,7 @@
 
 // Attributes of a favicon. A favicon is represented either with an image or
 // with a fallback monogram of a given color and background color.
-@interface FaviconAttributes : NSObject <NSCoding>
+@interface FaviconAttributes : NSObject <NSSecureCoding>
 
 // Favicon image. Can be nil. If it is nil, monogram string and color are
 // guaranteed to be not nil.
diff --git a/ios/chrome/common/ui/favicon/favicon_attributes.mm b/ios/chrome/common/ui/favicon/favicon_attributes.mm
index c582884..b9fa4996 100644
--- a/ios/chrome/common/ui/favicon/favicon_attributes.mm
+++ b/ios/chrome/common/ui/favicon/favicon_attributes.mm
@@ -61,13 +61,21 @@
 
 #pragma mark - NSCoding
 
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
   UIImage* faviconImage =
-      [UIImage imageWithData:[aDecoder decodeObjectForKey:kFaviconImageKey]];
-  NSString* monogramString = [aDecoder decodeObjectForKey:kFaviconMonogramKey];
-  UIColor* textColor = [aDecoder decodeObjectForKey:kFaviconTextColorKey];
+      [UIImage imageWithData:[aDecoder decodeObjectOfClass:[NSData class]
+                                                    forKey:kFaviconImageKey]];
+  NSString* monogramString = [aDecoder decodeObjectOfClass:[NSString class]
+                                                    forKey:kFaviconMonogramKey];
+  UIColor* textColor = [aDecoder decodeObjectOfClass:[UIColor class]
+                                              forKey:kFaviconTextColorKey];
   UIColor* backgroundColor =
-      [aDecoder decodeObjectForKey:kFaviconBackgroundColorKey];
+      [aDecoder decodeObjectOfClass:[UIColor class]
+                             forKey:kFaviconBackgroundColorKey];
   if (faviconImage || (monogramString && textColor && backgroundColor)) {
     return [self initWithImage:faviconImage
                       monogram:monogramString
Loading diff…

Original Bug Report

reported by [email protected]

Potential Sandbox Escape via Insecure Deserialization of App Group Data

Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: Multiple iOS Chrome components process data from the shared App Group container using NSKeyedUnarchiver with requiresSecureCoding explicitly disabled. A compromised App Extension can write malicious serialized objects to this shared storage. When the Main Browser process reads and deserializes this data, it can lead to arbitrary code execution (RCE) and a sandbox escape.

Affected files:

  • ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm
  • ios/chrome/browser/share_extension/model/share_extension_utils.mm
  • ios/chrome/credential_provider_extension/favicon_util.mm

Estimated timestamp from git blame: 2025-07-25

Summary

A potential sandbox escape vulnerability exists in iOS Chrome due to the insecure deserialization of data retrieved from the shared App Group container.

App Extensions (such as the Widget Extension or Share Extension) operate in a restricted sandbox but share access to an “App Group” container with the Main Chrome Browser app for data synchronization. In several places, the Main Browser process reads data from this shared container and deserializes it using NSKeyedUnarchiver with requiresSecureCoding explicitly set to NO.

Because the App Group container is writable by the less privileged App Extensions, an attacker who compromises an extension can write a maliciously crafted serialized Objective-C object graph into the shared storage. When the fully privileged Main Browser process deserializes this payload without secure coding enforced, it will instantiate any class specified by the attacker, potentially leading to Arbitrary Code Execution (RCE) via standard Objective-C gadget chains.

Technical Details

The vulnerability manifests in at least two distinct paths where the Main Browser process reads from the shared container:

Path 1: Content Suggestions (NTP Tiles) In ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm, the DecodeData function explicitly disables secure coding:

NSDictionary* DecodeData(NSData* data) {
  // ... initialization ...
  unarchiver.requiresSecureCoding = NO;
  return [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
}

This function is called by a background task ClearOutdatedIcons(), which reads the dictionary stored at app_group::kSuggestedItemsForMultiprofile in the shared NSUserDefaults. A compromised extension can poison this key.

Path 2: Share Extension Processing In ios/chrome/browser/share_extension/model/share_extension_utils.mm, the PerformBlockingFileReadAndParse function reads files from the shared ExternalCommands folder:

ParsedShareExtensionEntry* PerformBlockingFileReadAndParse(NSURL* file_url) {
  // ... initialization ...
  unarchiver.requiresSecureCoding = NO;
  id entryID = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
  // ... 
}

This is called by ShareExtensionController::handleFileAtURL:, which monitors a shared folder that the Share Extension writes to.

Potential Attack Steps

Note: These steps describe a potential attack path; we do not currently have a working proof of concept to verify execution.

  1. Initial Compromise: An attacker exploits a separate vulnerability to gain initial code execution within a sandboxed iOS App Extension belonging to Chrome (e.g., the Widget or Share extension).
  2. Payload Generation: The attacker crafts a malicious serialized Objective-C object graph (NSData payload) utilizing known deserialization gadget chains.
  3. Data Poisoning: Running within the extension, the attacker writes the NSData payload to the shared App Group container. This could be writing to the app_group::kSuggestedItemsForMultiprofile key in the shared NSUserDefaults, or writing a file to the shared ExternalCommands directory.
  4. Triggering the Main App: The user interacts with the Main Chrome Browser app, triggering actions that read the shared data (e.g., opening the New Tab Page triggers the tile saver background task; the ShareExtensionController automatically detects new files).
  5. Insecure Deserialization: The Main Browser process reads the poisoned data and passes it to NSKeyedUnarchiver.
  6. Arbitrary Code Execution: Because requiresSecureCoding = NO, the unarchiver allocates and initializes the attacker’s gadget classes. The subsequent execution of methods like initWithCoder: or dealloc provides Arbitrary Code Execution within the highly privileged Main Browser process, completing the sandbox escape.

Recommendation

To remediate this issue, data crossing process boundaries must enforce secure coding:

  1. Ensure that all classes serialized and stored in the App Group container (such as NTPTile and FaviconAttributes) implement the NSSecureCoding protocol.
  2. Audit all usages of NSKeyedUnarchiver that process data from shared containers.
  3. Set unarchiver.requiresSecureCoding = YES;.
  4. Use decodeObjectOfClasses:forKey: or decodeObjectOfClass:forKey: to specify an explicit allowlist of expected classes during deserialization, rather than the overly permissive decodeObjectForKey:.

Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker