CVE-2026-17762
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
BrowserCoordinatorTestios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm |
modified | |
ifios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.mm |
modified |
Files Changed
ios/chrome/browser/browser_view/ui_bundled/browser_coordinator.mmios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mmios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.mm
Patch
From 7be45fdf6be294e2e8877432f0640a93b9dda044 Mon Sep 17 00:00:00 2001 From: Olivier ROBIN <[email protected]> Date: Tue, 09 Jun 2026 05:56:15 -0700 Subject: [PATCH] Use the correct iframe to download the images Use the context menu info to use the correct iframe to load the image on long press. Bug: 508251844 Change-Id: Ifcf019d97acb5fd9d8ce581273361c212855dc97 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7882945 Reviewed-by: Gauthier Ambard <[email protected]> Commit-Queue: Olivier Robin <[email protected]> Reviewed-by: Mike Dougherty <[email protected]> Cr-Commit-Position: refs/heads/main@{#1643888} --- diff --git a/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator.mm b/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator.mm index de6a5f3b..36289456 100644 --- a/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator.mm +++ b/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator.mm @@ -4610,7 +4610,9 @@ browser:self.browser imageURL:command.imageURL referrer:command.referrer - webState:command.webState.get()]; + webState:command.webState.get() + frameID:command.frameID + frameOrigin:command.frameOrigin]; [self.saveToPhotosCoordinator start]; } diff --git a/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm b/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm index 3364f12..43601a1 100644 --- a/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm +++ b/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm @@ -87,6 +87,7 @@ #import "third_party/ocmock/OCMock/OCMock.h" #import "third_party/ocmock/gtest_support.h" #import "ui/base/device_form_factor.h" +#import "url/origin.h" // Test fixture for BrowserCoordinator testing. class BrowserCoordinatorTest : public PlatformTest { @@ -442,10 +443,12 @@ GURL fakeImageURL("http://www.example.com/image.jpg"); web::Referrer fakeImageReferrer; web::WebState* webState = GetActiveWebState(); - SaveImageToPhotosCommand* command = - [[SaveImageToPhotosCommand alloc] initWithImageURL:fakeImageURL - referrer:fakeImageReferrer - webState:webState]; + SaveImageToPhotosCommand* command = [[SaveImageToPhotosCommand alloc] + initWithImageURL:fakeImageURL + referrer:fakeImageReferrer + webState:webState + frameID:"fake_frame_id" + frameOrigin:url::Origin::Create(GURL("http://chromium.test/"))]; // Tests that -[BrowserCoordinator saveImageToPhotos:] starts the // SaveToPhotosCoordinator. @@ -456,7 +459,9 @@ browser:browser_.get() imageURL:command.imageURL referrer:command.referrer - webState:command.webState.get()]) + webState:command.webState.get() + frameID:command.frameID + frameOrigin:command.frameOrigin]) .andReturn(mockSaveToPhotosCoordinator); OCMExpect([(SaveToPhotosCoordinator*)mockSaveToPhotosCoordinator start]); [handler saveImageToPhotos:command]; diff --git a/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.mm b/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.mm index c5a104da..0ded7866 100644 --- a/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.mm +++ b/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.mm @@ -353,7 +353,9 @@ ImagePreviewViewController* previewViewController = [[ImagePreviewViewController alloc] initWithSrcURL:net::NSURLWithGURL(params.src_url) - webState:webState]; + webState:webState + frameID:base::SysUTF8ToNSString(params.frame_id) + frameOrigin:params.frame_security_origin]; [previewViewController loadPreview]; return ^() { return previewViewController; @@ -541,12 +543,16 @@ [self imageSavingElementsWithURL:imageURL scenario:scenario referrer:referrer - webState:webState]; + webState:webState + params:params]; [imageMenuElements addObjectsFromArray:imageSavingElements]; // Copy Image. UIAction* copyImage = [actionFactory actionCopyImageWithBlock:^{ - [weakSelf copyImageAtURL:imageURL referrer:referrer]; + [weakSelf copyImageAtURL:imageURL + referrer:referrer + frameID:params.frame_id + frameOrigin:params.frame_security_origin]; }]; [imageMenuElements addObject:copyImage]; @@ -564,7 +570,8 @@ NSArray<UIMenuElement*>* imageSearchingElements = [self imageSearchingElementsWithURL:imageURL scenario:scenario - referrer:referrer]; + referrer:referrer + params:params]; // Launch the Gemini experience with an image attached. UIMenuElement* geminiElement = nil; @@ -592,7 +599,9 @@ RecordImageRemixContextMenuEntryPointShown(); ProceduralBlock geminiElementCallback = ^{ - [weakSelf openGeminiWithImageURL:imageURL referrer:referrer]; + [weakSelf openGeminiWithImageURL:imageURL + referrer:referrer + params:params]; }; geminiElement = [actionFactory actionToOpenImageInGeminiWithBlock:geminiElementCallback]; @@ -630,25 +639,29 @@ // Lens. - (void)searchImageWithURL:(GURL)imageURL usingLens:(BOOL)usingLens - referrer:(web::Referrer)referrer { + referrer:(web::Referrer)referrer + params:(web::ContextMenuParams)params { ImageFetchTabHelper* imageFetcher = ImageFetchTabHelper::FromWebState(self.webState); DCHECK(imageFetcher); __weak ContextMenuConfigurationProvider* weakSelf = self; - imageFetcher->GetImageData(imageURL, referrer, ^(NSData* rawData) { - // Arbitrary web image data requires sanitization before use. - [weakSelf sanitizeImageData:rawData - mimeType:kJPEGImageMimeType - completion:^(NSData* transcodedData) { - if (usingLens) { - [weakSelf searchImageUsingLensWithData:transcodedData]; - } else { - [weakSelf searchByImageData:transcodedData - imageURL:imageURL]; - } - }]; - }); + imageFetcher->GetImageData( + imageURL, referrer, params.frame_id, params.frame_security_origin, + ^(NSData* rawData) { + // Arbitrary web image data requires sanitization before use. + [weakSelf + sanitizeImageData:rawData + mimeType:kJPEGImageMimeType + completion:^(NSData* transcodedData) { + if (usingLens) { + [weakSelf searchImageUsingLensWithData:transcodedData]; + } else { + [weakSelf searchByImageData:transcodedData + imageURL:imageURL]; + } + }]; + }); } // Sanitizes a web image data before use by passing it through the transcoder. @@ -820,7 +833,8 @@ imageSavingElementsWithURL:(GURL)imageURL scenario:(MenuScenarioHistogram)scenario referrer:(web::Referrer)referrer - webState:(web::WebState*)webState { + webState:(web::WebState*)webState + params:(web::ContextMenuParams)params { // TODO(crbug.com/351817704): Save to photo is not presented in the // baseViewController. const bool saveToPhotosAvailable = @@ -844,6 +858,8 @@ [strongSelf.imageSaver saveImageAtURL:imageURL referrer:referrer webState:strongSelf.webState + frameID:params.frame_id + frameOrigin:params.frame_security_origin baseViewController:strongSelf.baseViewController]; base::UmaHistogramEnumeration( kSaveToPhotosContextMenuActionsHistogram, @@ -881,6 +897,7 @@ actionToSaveToPhotosWithImageURL:imageURL referrer:referrer webState:webState + params:params block:^{ base::UmaHistogramEnumeration( kSaveToPhotosContextMenuActionsHistogram, @@ -911,7 +928,8 @@ - (NSArray<UIMenuElement*>*)
Regression Test / PoC
diff --git a/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm b/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm
index 3364f12..43601a1 100644
--- a/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm
+++ b/ios/chrome/browser/browser_view/ui_bundled/browser_coordinator_unittest.mm
@@ -87,6 +87,7 @@
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "ui/base/device_form_factor.h"
+#import "url/origin.h"
// Test fixture for BrowserCoordinator testing.
class BrowserCoordinatorTest : public PlatformTest {
@@ -442,10 +443,12 @@
GURL fakeImageURL("http://www.example.com/image.jpg");
web::Referrer fakeImageReferrer;
web::WebState* webState = GetActiveWebState();
- SaveImageToPhotosCommand* command =
- [[SaveImageToPhotosCommand alloc] initWithImageURL:fakeImageURL
- referrer:fakeImageReferrer
- webState:webState];
+ SaveImageToPhotosCommand* command = [[SaveImageToPhotosCommand alloc]
+ initWithImageURL:fakeImageURL
+ referrer:fakeImageReferrer
+ webState:webState
+ frameID:"fake_frame_id"
+ frameOrigin:url::Origin::Create(GURL("http://chromium.test/"))];
// Tests that -[BrowserCoordinator saveImageToPhotos:] starts the
// SaveToPhotosCoordinator.
@@ -456,7 +459,9 @@
browser:browser_.get()
imageURL:command.imageURL
referrer:command.referrer
- webState:command.webState.get()])
+ webState:command.webState.get()
+ frameID:command.frameID
+ frameOrigin:command.frameOrigin])
.andReturn(mockSaveToPhotosCoordinator);
OCMExpect([(SaveToPhotosCoordinator*)mockSaveToPhotosCoordinator start]);
[handler saveImageToPhotos:command];
diff --git a/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider_unittest.mm b/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider_unittest.mm
index 3300484..ef19fb5d 100644
--- a/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider_unittest.mm
+++ b/ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider_unittest.mm
@@ -51,6 +51,7 @@
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/l10n/l10n_util.h"
+#import "url/origin.h"
namespace {
@@ -87,6 +88,9 @@
web::ContextMenuParams GetContextMenuParamsWithImageUrl(const char* image_url) {
web::ContextMenuParams params;
params.src_url = GURL(image_url);
+ params.frame_id = "fake_frame_id";
+ params.frame_security_origin =
+ url::Origin::Create(GURL("https://allowed.com/"));
return params;
}
@@ -259,6 +263,7 @@
[actionFactory actionToSaveToPhotosWithImageURL:GURL(kImageUrl)
referrer:web::Referrer()
webState:GetActiveWebState()
+ params:paramsWithImage
block:nil];
// Test that there is an element with the expected title in the submenu.
@@ -309,6 +314,7 @@
[actionFactory actionToSaveToPhotosWithImageURL:GURL(kImageUrl)
referrer:web::Referrer()
webState:GetActiveWebState()
+ params:paramsWithImage
block:nil];
// Test that there is an element with the expected title in the submenu.
@@ -520,6 +526,7 @@
[actionFactory actionToSaveToPhotosWithImageURL:GURL(kImageUrl)
referrer:web::Referrer()
webState:GetActiveWebState()
+ params:paramsWithImage
block:nil];
// Test that there is an element with the expected title in the submenu for
diff --git a/ios/chrome/browser/menu/ui_bundled/browser_action_factory_unittest.mm b/ios/chrome/browser/menu/ui_bundled/browser_action_factory_unittest.mm
index 265d48e..8ac6dc70 100644
--- a/ios/chrome/browser/menu/ui_bundled/browser_action_factory_unittest.mm
+++ b/ios/chrome/browser/menu/ui_bundled/browser_action_factory_unittest.mm
@@ -28,6 +28,7 @@
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_task_environment.h"
+#import "ios/web/public/ui/context_menu_params.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
@@ -37,6 +38,7 @@
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/base/test/ios/ui_image_test_utils.h"
#import "url/gurl.h"
+#import "url/origin.h"
namespace {
const MenuScenarioHistogram kTestMenuScenario =
@@ -456,10 +458,16 @@
web::Referrer fakeImageReferrer;
std::unique_ptr<web::WebState> fakeWebState =
std::make_unique<web::FakeWebState>();
+ web::ContextMenuParams fakeParams;
+ fakeParams.frame_id = "fake_frame_id";
+ fakeParams.frame_security_origin =
+ url::Origin::Create(GURL("http://chromium.test/"));
+
UIAction* action =
[factory actionToSaveToPhotosWithImageURL:fakeImageURL
referrer:fakeImageReferrer
webState:fakeWebState.get()
+ params:fakeParams
block:nil];
EXPECT_NSEQ(expectedTitle, action.title);
diff --git a/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_coordinator_unittest.mm b/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_coordinator_unittest.mm
index a51db18..162c54b 100644
--- a/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_coordinator_unittest.mm
+++ b/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_coordinator_unittest.mm
@@ -46,6 +46,7 @@
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
+#import "url/origin.h"
namespace {
@@ -161,7 +162,10 @@
browser:browser_.get()
imageURL:GURL(kFakeImageUrl)
referrer:web::Referrer()
- webState:GetActiveWebState()];
+ webState:GetActiveWebState()
+ frameID:"fake_frame_id"
+ frameOrigin:url::Origin::Create(
+ GURL("http://chromium.test/"))];
}
// Returns the browser's active web state.
@@ -224,7 +228,9 @@
OCMExpect([[mock_save_to_photos_mediator_ ignoringNonObjectArgs]
startWithImageURL:GURL()
referrer:web::Referrer()
- webState:GetActiveWebState()]);
+ webState:GetActiveWebState()
+ frameID:""
+ frameOrigin:url::Origin()]);
[coordinator start];
EXPECT_OCMOCK_VERIFY(mock_save_to_photos_mediator_);
diff --git a/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_mediator_unittest.mm b/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_mediator_unittest.mm
index 92cfd91b..173cb2ba 100644
--- a/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_mediator_unittest.mm
+++ b/ios/chrome/browser/save_to_photos/ui_bundled/save_to_photos_mediator_unittest.mm
@@ -6,6 +6,8 @@
#import <UIKit/UIKit.h>
+#import <string>
+
#import "base/functional/callback_helpers.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
@@ -47,6 +49,7 @@
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "ui/base/l10n/l10n_util_mac.h"
+#import "url/origin.h"
namespace {
@@ -108,6 +111,8 @@
// ImageFetchTabHelper
void GetImageData(const GURL& url,
const web::Referrer& referrer,
+ const std::string& frame_id,
+ const url::Origin& frame_origin,
ImageDataCallback callback) override {
get_image_data_called_ = true;
image_url_ = url;
@@ -230,6 +235,16 @@
ImageFetchTabHelper::FromWebState(web_state_.get()));
}
+ // Starts the mediator with standard parameters.
+ void StartMediator(SaveToPhotosMediator* mediator) {
+ [mediator
+ startWithImageURL:GURL(kFakeImageUrl)
+ referrer:web::Referrer()
+ webState:web_state_.get()
+ frameID:"fake_frame_id"
+ frameOrigin:url::Origin::Create(GURL("http://chromium.test/"))];
+ }
+
web::WebTaskEnvironment task_environment_{
web::WebTaskEnvironment::MainThreadType::IO};
// ScopedTestingLocalState needed for the authentication service and profile
@@ -255,9 +270,7 @@
TEST_F(SaveToPhotosMediatorTest, StartGetsImageData) {
// Create and start mediator.
SaveToPhotosMediator* mediator = CreateSaveToPhotosMediator();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
// Test that the image fetch tab helper was called with the given image URL.
FakeImageFetchTabHelper* image_fetch_tab_helper =
@@ -306,9 +319,7 @@
// Start the mediator and run until the image has been fetched and
// processed by the mediator.
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
task_environment_.RunUntilQuit();
EXPECT_OCMOCK_VERIFY(mock_save_to_photos_mediator_delegate);
@@ -340,9 +351,7 @@
// Start the mediator and run until the image has been fetched and
// processed by the mediator.
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
task_environment_.RunUntilQuit();
histogram_tester_.ExpectUniqueSample(
@@ -378,9 +387,7 @@
// Start the mediator and run until the image has been fetched and processed
// by the mediator.
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
task_environment_.RunUntilQuit();
// Test that the PhotosService has not been used to upload an image yet.
@@ -448,9 +455,7 @@
// Start the mediator and run until the image has been fetched and processed
// by the mediator.
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
task_environment_.RunUntilQuit();
// Test that the PhotosService has not been used to upload an image yet.
@@ -494,9 +499,7 @@
// Start the mediator and run until the image has been fetched and processed
// by the mediator.
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
task_environment_.RunUntilQuit();
// Expect the success snackbar (with a non-nil completion) is shown and save
@@ -584,9 +587,7 @@
// Start the mediator and run until the image has been fetched and processed
// by the mediator.
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
task_environment_.RunUntilQuit();
// Expect the success snackbar (with a non-nil completion) is shown and save
@@ -667,9 +668,7 @@
// Start the mediator and run until the image has been fetched and processed
// by the mediator.
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
task_environment_.RunUntilQuit();
// Set up the PhotosService to simulate upload failure.
@@ -737,9 +736,7 @@
showReauthForIdentity:fake_identity_]);
SetUpImageFetchTabHelperQuitClosure();
- [mediator startWithImageURL:GURL(kFakeImageUrl)
- referrer:web::Referrer()
- webState:web_state_.get()];
+ StartMediator(mediator);
... (truncated)
Original Bug Report
Potential Cross-Origin Information Leak and UI Spoofing via Context Menu Image Fetch on iOS
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 https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic flaw in iOS Chrome’s context menu image fetching unconditionally executes retrieval JavaScript in the main frame. If a user interacts with an image in a cross-origin iframe, an attacker-controlled main frame can intercept the fallback network request via a Service Worker. This leaks the cross-origin image URL and allows the attacker to spoof the image data that is copied or saved.
Affected files:
ios/chrome/browser/web/model/image_fetch/image_fetch_java_script_feature.mmios/chrome/browser/web/model/image_fetch/resources/image_fetch.tsios/chrome/browser/web/model/image_fetch/image_fetch_tab_helper.mmios/web/public/ui/context_menu_params.h
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A logic flaw in the image fetching mechanism for context menu actions on iOS allows for cross-origin information leaks and UI spoofing. When a user interacts with an image via the context menu (e.g., ‘Copy Image’ or ‘Save Image’), the browser attempts to retrieve the image data using a combination of native code and injected JavaScript.
The vulnerability resides in ImageFetchJavaScriptFeature::GetImageData, which is responsible for injecting the JavaScript retrieval script. It unconditionally targets the main frame of the WebState:
void ImageFetchJavaScriptFeature::GetImageData(web::WebState* web_state,
int call_id,
const GURL& url) {
web::WebFrame* main_frame = GetWebFramesManager(web_state)->GetMainWebFrame();
if (!main_frame) {
return;
}
// ...
CallJavaScriptFunction(main_frame, "imageFetch.getImageData", parameters);
}
If the image being interacted with is located within a cross-origin iframe, the retrieval script (image_fetch.ts) is still executed in the context of the main frame (albeit in an isolated world).
The script first attempts to find the image in the frame’s document.images to draw it to a canvas. Because the script runs in the main frame, it cannot find the image residing in the subframe. It then falls back to performing an XMLHttpRequest (XHR) for the image URL.
Potential Exploitation Steps
An attacker can exploit this by setting up a malicious main page embedding a cross-origin victim iframe. Note that these are suggested steps based on code analysis, as our tooling agent cannot execute code.
- Setup: The attacker hosts a malicious website and registers a Service Worker that listens for
fetchevents. - Embed Target: The page embeds a cross-origin iframe (e.g.,
https://victim.com) containing a sensitive image (e.g.,https://victim.com/image.png?token=secret). - Interaction: The attacker uses clickjacking to trick the user into long-pressing the sensitive image and selecting ‘Copy Image’ or ‘Save Image’.
- Flawed Injection: The native code calls
ImageFetchJavaScriptFeature::GetImageData, which injectsimageFetch.getImageDatainto the main frame. - Fallback Triggered: The script fails to find the image in the main frame’s DOM and initiates an XHR to
https://victim.com/image.png?token=secret. - Interception & Leak: Because the XHR is initiated by the main frame, it is intercepted by the attacker’s Service Worker. The Service Worker accesses
event.request.url, successfully leaking the cross-origin URL (including any sensitive tokens) to the attacker. - UI Spoofing: To perform UI spoofing, the Service Worker responds to the intercepted fetch event with a synthesized, attacker-controlled image blob and a wildcard CORS header (
Access-Control-Allow-Origin: *). - Completion: The XHR completes successfully. The script base64-encodes the attacker’s spoofed blob and sends it back to the native C++ browser process, which proceeds to copy or save the spoofed image instead of the original.
Suggested Fix
- Update
web::ContextMenuParamsto include theframe_idof the frame where the context menu was triggered. - Modify
ImageFetchJavaScriptFeature::GetImageDatato utilize thisframe_idto retrieve the correctweb::WebFrameviaGetWebFramesManager(web_state)->GetFrameWithId(...). - Execute the JavaScript only within the context of the specific frame that originated the interaction, ensuring the DOM lookup succeeds and any fallback requests are properly scoped to the correct origin.
Evaluated with Chrome root at commit: cc901875d53bf4e4fe0e01f02843871da4106e70
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.