CVE-2026-106279
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/chrome/browser/credential_provider/model/credential_provider_util.mm |
modified | |
forios/chrome/browser/credential_provider/model/credential_provider_util.mm |
modified | |
ifios/chrome/common/credential_provider/archivable_credential.mm |
modified | |
TEST_Fios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm |
modified |
Files Changed
ios/chrome/common/credential_provider/archivable_credential.mmios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mmios/chrome/browser/credential_provider/model/credential_provider_util.mmios/chrome/browser/credential_provider/model/credential_provider_util.h
Patch
From 3c08b52a99c1610c6882ddd1e9fbb6c128112298 Mon Sep 17 00:00:00 2001 From: Sourav <[email protected]> Date: Thu, 10 Sep 2026 07:39:12 -0700 Subject: [PATCH] [iOS] Enforce SHA-256 format for favicon file keys Verifies that favicon file keys adhere to the 64-character hex digest format produced by GetFaviconFileKey before fetching or saving favicons. Bug: 513757840 Change-Id: I47fb9ae169703bc2115f5fd107dc708f59de4206 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8376404 Commit-Queue: Sourav Uttam Sinha <[email protected]> Reviewed-by: Alexis Hétu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1695441} --- diff --git a/ios/chrome/browser/credential_provider/model/credential_provider_util.h b/ios/chrome/browser/credential_provider/model/credential_provider_util.h index 97cf1c34..7914192 100644 --- a/ios/chrome/browser/credential_provider/model/credential_provider_util.h +++ b/ios/chrome/browser/credential_provider/model/credential_provider_util.h @@ -34,6 +34,9 @@ // Returns the favicon file key. NSString* GetFaviconFileKey(const GURL& url); +// Returns whether `key` is a valid favicon file key. +bool IsValidFaviconFileKey(NSString* key); + // Update favicons in the Chrome app group storage. void UpdateFaviconsStorageForProfile(base::WeakPtr<ProfileIOS> weak_profile, bool fallback_to_google_server); diff --git a/ios/chrome/browser/credential_provider/model/credential_provider_util.mm b/ios/chrome/browser/credential_provider/model/credential_provider_util.mm index 96ca470..edc15f0 100644 --- a/ios/chrome/browser/credential_provider/model/credential_provider_util.mm +++ b/ios/chrome/browser/credential_provider/model/credential_provider_util.mm @@ -6,6 +6,8 @@ #import <CommonCrypto/CommonDigest.h> +#import <array> + #import "base/apple/backup_util.h" #import "base/apple/foundation_util.h" #import "base/check_is_test.h" @@ -13,6 +15,7 @@ #import "base/no_destructor.h" #import "base/strings/strcat.h" #import "base/strings/string_number_conversions.h" +#import "base/strings/string_util.h" #import "base/strings/sys_string_conversions.h" #import "base/strings/utf_string_conversions.h" #import "base/synchronization/lock.h" @@ -130,8 +133,27 @@ return base::SysUTF8ToNSString(base::HexEncode(result)); } +bool IsValidFaviconFileKey(NSString* key) { + constexpr NSUInteger kFaviconKeyLength = CC_SHA256_DIGEST_LENGTH * 2; + if (!key || key.length != kFaviconKeyLength) { + return false; + } + std::array<unichar, kFaviconKeyLength> buffer; + [key getCharacters:buffer.data() range:NSMakeRange(0, kFaviconKeyLength)]; + + for (NSUInteger i = 0; i < kFaviconKeyLength; ++i) { + if (!base::IsHexDigit(buffer[i])) { + return false; + } + } + return true; +} + void SaveFaviconToSharedAppContainer(FaviconAttributes* attributes, NSString* filename) { + if (!IsValidFaviconFileKey(filename)) { + return; + } base::OnceCallback<void()> write_image = base::BindOnce(^{ NSError* error = nil; NSData* data = [NSKeyedArchiver archivedDataWithRootObject:attributes @@ -220,6 +242,9 @@ bool fallback_to_google_server) { DCHECK(favicon_loader); DCHECK(filename); + if (!IsValidFaviconFileKey(filename)) { + return; + } if (skip_max_verification) { ContinueFetchingFavicon(favicon_loader->AsWeakPtr(), site_url, filename, fallback_to_google_server, @@ -348,8 +373,8 @@ continue; } NSString* filename = credential.favicon; - if (!credential.favicon) { - // Add favicon name to the credential and update the store. + if (!IsValidFaviconFileKey(filename)) { + // Ensure the credential has an updated favicon key and update the store. filename = GetFaviconFileKey(url); ArchivableCredential* newCredential = [[ArchivableCredential alloc] initWithFavicon:filename diff --git a/ios/chrome/common/credential_provider/archivable_credential.mm b/ios/chrome/common/credential_provider/archivable_credential.mm index e35fc66..7ec88a0 100644 --- a/ios/chrome/common/credential_provider/archivable_credential.mm +++ b/ios/chrome/common/credential_provider/archivable_credential.mm @@ -87,8 +87,8 @@ - (instancetype)initWithFavicon:(NSString*)favicon credential:(id<Credential>)credential { if (credential.isPasskey) { - // Use the passkey initilizer - self = [self initWithFavicon:credential.favicon + // Use the passkey initializer + self = [self initWithFavicon:favicon gaia:credential.gaia recordIdentifier:credential.recordIdentifier syncId:credential.syncId @@ -106,7 +106,7 @@ editedByUser:credential.editedByUser]; } else { // Use the password initializer - self = [self initWithFavicon:credential.favicon + self = [self initWithFavicon:favicon gaia:credential.gaia password:credential.password rank:credential.rank diff --git a/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm b/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm index f6295de..99575d1d 100644 --- a/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm +++ b/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm @@ -157,4 +157,25 @@ RecordIdentifierForPasswordForm(form)); } +// Tests that IsValidFaviconFileKey correctly validates favicon key hashes. +TEST_F(CredentialProviderUtilTest, IsValidFaviconFileKey) { + // Valid 64-character hex key. + NSString* valid_key = + @"BD7639C34EA3480A8AAD704306C8870161761506AD948AC6FA037B83CFF22D37"; + EXPECT_TRUE(IsValidFaviconFileKey(valid_key)); + EXPECT_TRUE(IsValidFaviconFileKey([valid_key lowercaseString])); + + // Nil or empty. + EXPECT_FALSE(IsValidFaviconFileKey(nil)); + EXPECT_FALSE(IsValidFaviconFileKey(@"")); + + // Length mismatch. + EXPECT_FALSE(IsValidFaviconFileKey(@"ABCD")); + EXPECT_FALSE(IsValidFaviconFileKey([valid_key stringByAppendingString:@"A"])); + + // Non-hex character. + EXPECT_FALSE(IsValidFaviconFileKey( + @"ZZ7639C34EA3480A8AAD704306C8870161761506AD948AC6FA037B83CFF22D37")); +} + } // namespace
Regression Test / PoC
diff --git a/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm b/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm
index f6295de..99575d1d 100644
--- a/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm
+++ b/ios/chrome/browser/credential_provider/model/credential_provider_util_unittest.mm
@@ -157,4 +157,25 @@
RecordIdentifierForPasswordForm(form));
}
+// Tests that IsValidFaviconFileKey correctly validates favicon key hashes.
+TEST_F(CredentialProviderUtilTest, IsValidFaviconFileKey) {
+ // Valid 64-character hex key.
+ NSString* valid_key =
+ @"BD7639C34EA3480A8AAD704306C8870161761506AD948AC6FA037B83CFF22D37";
+ EXPECT_TRUE(IsValidFaviconFileKey(valid_key));
+ EXPECT_TRUE(IsValidFaviconFileKey([valid_key lowercaseString]));
+
+ // Nil or empty.
+ EXPECT_FALSE(IsValidFaviconFileKey(nil));
+ EXPECT_FALSE(IsValidFaviconFileKey(@""));
+
+ // Length mismatch.
+ EXPECT_FALSE(IsValidFaviconFileKey(@"ABCD"));
+ EXPECT_FALSE(IsValidFaviconFileKey([valid_key stringByAppendingString:@"A"]));
+
+ // Non-hex character.
+ EXPECT_FALSE(IsValidFaviconFileKey(
+ @"ZZ7639C34EA3480A8AAD704306C8870161761506AD948AC6FA037B83CFF22D37"));
+}
+
} // namespace
Original Bug Report
Path Traversal in iOS Favicon Synchronization via Shared App Group Store
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: A vulnerability in Chrome for iOS allows a compromised app extension to overwrite or delete files in the main app’s private data container. This is caused by insufficient sanitization of a ‘favicon’ filename retrieved from a shared App Group container. An attacker with control over an extension could use path traversal to target sensitive files like preferences or databases.
Affected files:
ios/chrome/browser/credential_provider/model/credential_provider_util.mmios/chrome/common/credential_provider/archivable_credential.mmios/chrome/common/credential_provider/archivable_credential_store.mmios/chrome/common/credential_provider/constants.mmios/chrome/common/app_group/app_group_constants.mmios/chrome/app/profile/profile_controller.mm
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A path traversal vulnerability has been identified in the way the main Chrome iOS app synchronizes favicons from the shared App Group container used by its extensions (e.g., Credential Provider, Share, or Widget extensions). The application reads an ArchivableCredentialStore from the shared container and uses the favicon field of each credential as a filename for local storage without validation.
Root Cause Analysis
In ios/chrome/browser/credential_provider/model/credential_provider_util.mm, the function UpdateFaviconsStorage() iterates through credentials synced from the shared container. It extracts the favicon property, which is an arbitrary NSString supplied by the extension:
// ios/chrome/browser/credential_provider/model/credential_provider_util.mm
NSString* filename = credential.favicon;
if (filename) {
FetchFaviconForURLToPath(favicon_loader, url, filename, ...);
}
This filename is eventually passed to SaveFaviconToSharedAppContainer(), which constructs a file URL using URLByAppendingPathComponent:. This method does not sanitize or reject path traversal segments like .. or /:
// ios/chrome/browser/credential_provider/model/credential_provider_util.mm
NSURL* shared_favicon_attributes_folder_url = GetFaviconsFolderURL();
NSURL* file_url = [shared_favicon_attributes_folder_url
URLByAppendingPathComponent:filename
isDirectory:NO];
// Overwrite the file outside the intended directory
[file_manager createFileAtPath:[file_url path]
contents:data
attributes:nil];
Because the main app runs with higher privileges and has full access to its own private container (/var/mobile/Containers/Data/Application/<MAIN_APP_UUID>/), an attacker can use strings like ../../../../Data/Application/<MAIN_APP_UUID>/Library/Preferences/com.google.chrome.ios.plist to overwrite critical configuration files.
Potential Attack Scenario
- Compromised Extension: An attacker first gains code execution in a Chrome iOS app extension (e.g., via a separate vulnerability).
- Identify Target: The attacker determines the main app’s data container UUID, potentially leaked via shared logs or files in the App Group container.
- Craft Store: The extension writes a malicious
ArchivableCredentialStoreto the shared container. This store contains a credential where thefaviconfield is a traversal path targeting a sensitive file in the main app’s container. - Trigger Sync: The attacker waits for the main app to initiate a favicon resync (occurring periodically or upon certain app states). During this sync, the main app processes the malicious store and performs the out-of-bounds write.
Impact
This vulnerability allows a compromised, lower-privilege extension to bypass sandbox restrictions and compromise the integrity of the main app’s private data. This could result in persistent data loss, corruption of login data, or modification of app preferences.
Suggested Fix
Implement strict validation of the favicon filename in ios/chrome/browser/credential_provider/model/credential_provider_util.mm. The filename should be verified to contain only safe characters (e.g., hexadecimal characters if SHA256 hashes are expected) and must not contain path separators (/) or dot-segments (..). Additionally, the code should verify that the resulting NSURL is still contained within the expected SharedFaviconAttributes directory.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.