CVE-2026-13850
Overview
Files Changed
ios/chrome/widget_kit_extension/shortcuts_widget.swift
Patch
From 406a0fa3ec41d15b6bd951692bb4d812e947f4fd Mon Sep 17 00:00:00 2001
From: Olivier Robin <olivierrobin@google.com>
Date: Thu, 04 Jun 2026 01:19:05 -0700
Subject: [PATCH] Use secureCoding for favicon decoding in shortcuts widget
Fixed: 517610676
Change-Id: I5ada70d48f792a160aaf287a06c8097f2f0dae50
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7895591
Commit-Queue: Olivier Robin <olivierrobin@chromium.org>
Reviewed-by: Robbie Gibson <rkgibson@google.com>
Cr-Commit-Position: refs/heads/main@{#1641522}
---
diff --git a/ios/chrome/widget_kit_extension/shortcuts_widget.swift b/ios/chrome/widget_kit_extension/shortcuts_widget.swift
index 94b99f1..75630fbd 100644
--- a/ios/chrome/widget_kit_extension/shortcuts_widget.swift
+++ b/ios/chrome/widget_kit_extension/shortcuts_widget.swift
@@ -229,10 +229,12 @@
guard let unarchiver = unarchiverForAccount
else { return emptyEntry }
- unarchiver.requiresSecureCoding = false
+ unarchiver.requiresSecureCoding = true
+ let allowedClasses = [NSDictionary.self, NSURL.self, NTPTile.self]
guard
- let mostVisitedSites = unarchiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey)
+ let mostVisitedSites = unarchiver.decodeObject(
+ of: allowedClasses, forKey: NSKeyedArchiveRootObjectKey)
as? [NSURL: NTPTile]
else {
return emptyEntry
@@ -287,16 +289,16 @@
// Create a chromewidgetkit:// url to open the given URL.
private func convertURL(url: URL) -> URL {
- let query_url = URLQueryItem(name: "url", value: url.absoluteString)
+ let queryUrl = URLQueryItem(name: "url", value: url.absoluteString)
var urlcomps = URLComponents(
url: WidgetConstants.ShortcutsWidget.open,
resolvingAgainstBaseURL: false)!
if entry.gaiaID == nil {
- urlcomps.queryItems = [query_url]
+ urlcomps.queryItems = [queryUrl]
} else {
// Add the gaia_id parameter only if available.
- let query_gaia = URLQueryItem(name: "gaia_id", value: entry.gaiaID)
- urlcomps.queryItems = [query_url, query_gaia]
+ let queryGaia = URLQueryItem(name: "gaia_id", value: entry.gaiaID)
+ urlcomps.queryItems = [queryUrl, queryGaia]
}
return urlcomps.url!
}
Original Bug Report
Insecure deserialization in shortcuts_widget.swift via shared defaults
Project Fortify, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The WidgetKit extension in iOS Chrome reads an NSData blob from the shared App Group user defaults and deserializes it with secure coding disabled. This potentially allows a compromised peer extension to trigger arbitrary class instantiation inside the WidgetKit extension process via NSKeyedUnarchiver. A counterpart implementation in the main app was previously hardened, but this Swift-based reader was missed.
Affected files:
ios/chrome/widget_kit_extension/shortcuts_widget.swift
Estimated timestamp from git blame: 2024-12-02
Summary
A potential insecure deserialization vulnerability exists in the Swift reader within the WidgetKit extension in iOS Chrome. Specifically, in ios/chrome/widget_kit_extension/shortcuts_widget.swift, the helper function loadMostVisitedSitesEntry() reads an NSData blob from the shared App-Group NSUserDefaults suite (under the key "SuggestedItemsForMIM"), disables secure coding, and decodes the root object without an allowlist of permitted classes.
Affected Files and Line Numbers
ios/chrome/widget_kit_extension/shortcuts_widget.swift(lines 221-239)
Vulnerable Code Detail
In shortcuts_widget.swift, the code retrieves the dictionary and decodes it as follows:
guard let data = sharedDefaults.object(forKey: "SuggestedItemsForMIM") as? [String: Data]
else { return emptyEntry }
var unarchiverForAccount: NSKeyedUnarchiver?
for (key, value) in data {
if gaia == key {
unarchiverForAccount = try? NSKeyedUnarchiver(forReadingFrom: value)
}
}
guard let unarchiver = unarchiverForAccount
else { return emptyEntry }
unarchiver.requiresSecureCoding = false // <-- Secure coding explicitly disabled
guard
let mostVisitedSites = unarchiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey)
as? [NSURL: NTPTile]
else {
return emptyEntry
}
The type cast to [NSURL: NTPTile] occurs only after the object graph has been fully navigated and deserialized. By the time the cast runs, NSKeyedUnarchiver has already invoked -initWithCoder: on the classes named in the archive, enabling potential arbitrary class instantiation and execution of any available deserialization gadget chains inside the sandboxed WidgetKit extension process.
Root Cause & Context
This is the Swift implementation corresponding to the same NSUserDefaults key that was previously hardened in the main application’s Objective-C counterpart (ios/chrome/browser/content_suggestions/ui/cells/content_suggestions_tile_saver.mm line 125). While the main app’s reader enforces secure coding and uses an allowlist of permitted classes (NSDictionary, NSURL, and NTPTile), this Swift reader in the WidgetKit extension was overlooked.
Potential Attack Scenario
(Note: These are potential steps; we do not have a working proof of concept successfully executed on a live device)
- An attacker gains code execution in a peer app extension belonging to the same App Group (e.g., the Share Extension, which processes untrusted user-provided content).
- The compromised peer extension writes a malicious serialized payload mapped to the active account’s GAIA ID into the shared
NSUserDefaultssuite under the key"SuggestedItemsForMIM". - To bypass expiration validation, the compromised peer extension also writes a current timestamp under the key
"SuggestedItemsLastModificationDateForMIM". - When a Shortcuts widget refresh occurs (either triggered by the OS or manually by the user), the WidgetKit extension loads the timeline and invokes
loadMostVisitedSitesEntry(). - The extension retrieves the malicious payload from the shared defaults, instantiates
NSKeyedUnarchiver, disables secure coding, and decodes the root object. - The unarchiver executes the gadget chain embedded in the payload during deserialization, potentially leading to arbitrary code execution inside the sandboxed WidgetKit extension process.
Suggested Fix
Update the Swift deserialization logic in shortcuts_widget.swift to enforce secure coding and specify a restricted set of allowed classes during decoding, matching the secure implementation used in the main app:
unarchiver.requiresSecureCoding = true
let allowedClasses = [NSDictionary.self, NSURL.self, NTPTile.self]
guard
let mostVisitedSites = unarchiver.decodeObject(of: allowedClasses, forKey: NSKeyedArchiveRootObjectKey)
as? [NSURL: NTPTile]
else {
return emptyEntry
}
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.