CVE-2026-95367
Overview
Background
- `DropData`
- the browser-side struct describing a drag payload, whose
filenamesfield holdsui::FileInfoentries each pairing an absolutepathwith an optionaldisplay_name. - `DropData::Metadata`
- a lightweight per-item descriptor sent to the renderer during drag-enter/drag-over that mirrors the drag contents without (ideally) exposing sensitive host details.
- Drag-enter/drag-over
- the pre-drop phase in which the browser forwards drag metadata to the renderer’s
blink::mojom::FrameWidget(viaDragTargetDragEnter) so the page can decide whether to accept the drop. - `base::FilePath::BaseName`
- a method returning only the final path component (the file name) with all leading directory components stripped.
Root Cause Analysis
When a drag operation entered a renderer’s viewport, the browser built a DropData::Metadata list for each dragged file and, on Aura, populated it with the file’s full absolute path via DropData::Metadata::CreateForFilePath(file_info.path, file_info.display_name). This metadata was delivered to the renderer during DragTargetDragEnter before the user committed to a drop, so a compromised or malicious renderer could read the complete host filesystem path of every hovered file (for example /home/user/secret/file.txt) without any user consent to share it. The invariant violated is that the pre-drop phase should expose only what is necessary to negotiate the drop, not sensitive host paths that are only meant to be transferred at drop time.
The fix replaces file_info.path with file_info.path.BaseName() when constructing the drag-enter metadata, so only the bare file name reaches the renderer; the untruncated absolute paths are still preserved and sent when the real drop event fires.
path in drag-enter/drag-over metadata that a renderer can observe before any drop is committed; the fix truncates that field to its BaseName so the directory structure never crosses the process boundary early.Attack Path
- Compromise the renderer An attacker gains code execution in a sandboxed renderer process (e.g., via a separate bug) so it can observe the raw Mojo drag messages. (inferred)
- Induce a drag The user drags one or more local files over content controlled by the attacker’s page, triggering the browser to build drag metadata.
- Leak absolute paths on drag-enter
The browser calls
DragTargetDragEnter, and the unsanitizedDropData::Metadatacarrying eachfile_info.path(e.g./absolute/path/to/file1.txt) is delivered to the renderer before the user drops. - Harvest host filesystem information
The compromised renderer reads the full paths from the received
blink::mojom::DragData, disclosing usernames, directory layout, and file locations that aid further exploitation. (inferred)
Impact Assessment
DataTransfer and does not state exploitation in the wild; any downstream use of the leaked paths to strengthen another exploit is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/renderer_host/render_widget_host_impl.cc |
modified | |
DragCaptureFrameWidgetcontent/browser/renderer_host/render_widget_host_unittest.cc |
modified | |
RenderWidgetHostDragTestcontent/browser/renderer_host/render_widget_host_unittest.cc |
modified | |
RenderWidgetHostDragTestcontent/browser/renderer_host/render_widget_host_unittest.cc |
modified | |
TEST_Fcontent/browser/renderer_host/render_widget_host_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/render_widget_host_unittest.cccontent/browser/renderer_host/render_widget_host_impl.cc
Audit Directions
- Pre-commit metadata exposureAudit every browser-to-renderer message sent during hover/enter/preview phases (drag, paste, file pickers) for full paths or other host secrets that should only travel at the final commit/drop step.
- Path fields crossing process boundariesSearch for
base::FilePathorpathmembers serialized into Mojo structs bound for a renderer and confirm each is reduced toBaseNameor an opaque token unless the full path is genuinely required. - Platform-specific branchesReview the
Aura/Windows/macOS drag code paths (and analogous#ifbranches elsewhere) since sanitization applied on one platform may be missing on others that populate the sameDropData::Metadata.
Patch
From 7c92200b7e5c8d2b233a9be68ebd7b54f41c5a4c Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> Date: Mon, 24 Aug 2026 08:56:20 -0700 Subject: [PATCH] [agy][content] Sanitize drag-enter file paths During drag-enter and drag-over events, the browser sends drag metadata to the renderer. Previously, this metadata included the absolute path of dragged files, which could be leaked to a compromised renderer process before the user performs a drop. This CL sanitizes the file paths in the metadata by replacing them with their BaseName. The full paths are still preserved and sent when the actual drop event occurs. Fixed: 514524620 Test: RenderWidgetHostDragTest.DragEnterDoesNotLeakPaths Change-Id: Ibd7b2d16a63de4028c4fa8d995a8a476c5374b92 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8257413 Reviewed-by: Alex Moshchuk <[email protected]> Commit-Queue: Tzarial <[email protected]> Cr-Commit-Position: refs/heads/main@{#1684792} --- diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc index c2e0532..953f9419 100644 --- a/content/browser/renderer_host/render_widget_host_impl.cc +++ b/content/browser/renderer_host/render_widget_host_impl.cc @@ -264,11 +264,13 @@ DropData::Kind::STRING, ui::kMimeTypeHtml16)); } - // On Aura, filenames are available before drop. + // On Aura, filenames are available before drop, but we sanitize the + // paths to their BaseName to prevent leaking absolute paths + // (https://crbug.com/514524620). for (const auto& file_info : drop_data.filenames) { if (!file_info.path.empty()) { metadata.push_back(DropData::Metadata::CreateForFilePath( - file_info.path, file_info.display_name)); + file_info.path.BaseName(), file_info.display_name)); } } diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc index 94ff324..5337b09e 100644 --- a/content/browser/renderer_host/render_widget_host_unittest.cc +++ b/content/browser/renderer_host/render_widget_host_unittest.cc @@ -51,6 +51,8 @@ #include "content/public/common/drop_data.h" #include "content/public/common/url_constants.h" #include "content/public/test/browser_task_environment.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/fake_frame_widget.h" #include "content/public/test/mock_render_process_host.h" #include "content/public/test/test_browser_context.h" #include "content/test/mock_render_input_router.h" @@ -2287,6 +2289,28 @@ } }; +class DragCaptureFrameWidget : public FakeFrameWidget { + public: + explicit DragCaptureFrameWidget( + mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver) + : FakeFrameWidget(std::move(receiver)) {} + + void DragTargetDragEnter(blink::mojom::DragDataPtr drag_data, + const gfx::PointF& point_in_viewport, + const gfx::PointF& screen_point, + blink::DragOperationsMask operations_allowed, + uint32_t key_modifiers, + DragTargetDragEnterCallback callback) override { + drag_data_ = std::move(drag_data); + std::move(callback).Run(ui::mojom::DragOperation::kCopy, true); + } + + const blink::mojom::DragDataPtr& drag_data() const { return drag_data_; } + + private: + blink::mojom::DragDataPtr drag_data_; +}; + class RenderWidgetHostDragTest : public RenderViewHostImplTestHarness { public: RenderWidgetHostDragTest() { @@ -2435,6 +2459,51 @@ FILE_PATH_LITERAL("payload.so")); } +TEST_F(RenderWidgetHostDragTest, DragEnterDoesNotLeakPaths) { + // Bind our mock frame widget. + mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver = + BindFakeFrameWidgetInterfaces(main_test_rfh()); + DragCaptureFrameWidget mock_frame_widget(std::move(receiver)); + + // Prepare drag data with files: one with display_name and one without. + DropData drop_data; + drop_data.filenames.emplace_back( + base::FilePath(FILE_PATH_LITERAL("/absolute/path/to/file1.txt")), + base::FilePath(FILE_PATH_LITERAL("display_name.txt"))); + drop_data.filenames.emplace_back( + base::FilePath(FILE_PATH_LITERAL("/another/absolute/path/to/file2.txt")), + base::FilePath()); + + // Call DragTargetDragEnter. + base::RunLoop run_loop; + GetRenderWidgetHost()->DragTargetDragEnter( + drop_data, gfx::PointF(), gfx::PointF(), + blink::DragOperationsMask::kDragOperationEvery, 0, + base::BindOnce( + [](base::OnceClosure quit_closure, ui::mojom::DragOperation operation, + bool document_is_handling_drag) { std::move(quit_closure).Run(); }, + run_loop.QuitClosure())); + run_loop.Run(); + + // Verify that the paths sent to renderer are sanitized to BaseName. + const auto& captured_drag_data = mock_frame_widget.drag_data(); + ASSERT_TRUE(captured_drag_data); + ASSERT_EQ(captured_drag_data->items.size(), 2u); + + const auto& item1 = captured_drag_data->items[0]; + ASSERT_TRUE(item1->is_file()); + EXPECT_EQ(item1->get_file()->path, + base::FilePath(FILE_PATH_LITERAL("file1.txt"))); + EXPECT_EQ(item1->get_file()->display_name, + base::FilePath(FILE_PATH_LITERAL("display_name.txt"))); + + const auto& item2 = captured_drag_data->items[1]; + ASSERT_TRUE(item2->is_file()); + EXPECT_EQ(item2->get_file()->path, + base::FilePath(FILE_PATH_LITERAL("file2.txt"))); + EXPECT_TRUE(item2->get_file()->display_name.empty()); +} + // A plain <img> drag on macOS populates `file_contents` but supplies neither a // source URL nor a Content-Disposition, so it must not surface as a File in the // renderer's DataTransfer.files. See crbug.com/522179938.
Regression Test / PoC
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index 94ff324..5337b09e 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -51,6 +51,8 @@
#include "content/public/common/drop_data.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/fake_frame_widget.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_browser_context.h"
#include "content/test/mock_render_input_router.h"
@@ -2287,6 +2289,28 @@
}
};
+class DragCaptureFrameWidget : public FakeFrameWidget {
+ public:
+ explicit DragCaptureFrameWidget(
+ mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver)
+ : FakeFrameWidget(std::move(receiver)) {}
+
+ void DragTargetDragEnter(blink::mojom::DragDataPtr drag_data,
+ const gfx::PointF& point_in_viewport,
+ const gfx::PointF& screen_point,
+ blink::DragOperationsMask operations_allowed,
+ uint32_t key_modifiers,
+ DragTargetDragEnterCallback callback) override {
+ drag_data_ = std::move(drag_data);
+ std::move(callback).Run(ui::mojom::DragOperation::kCopy, true);
+ }
+
+ const blink::mojom::DragDataPtr& drag_data() const { return drag_data_; }
+
+ private:
+ blink::mojom::DragDataPtr drag_data_;
+};
+
class RenderWidgetHostDragTest : public RenderViewHostImplTestHarness {
public:
RenderWidgetHostDragTest() {
@@ -2435,6 +2459,51 @@
FILE_PATH_LITERAL("payload.so"));
}
+TEST_F(RenderWidgetHostDragTest, DragEnterDoesNotLeakPaths) {
+ // Bind our mock frame widget.
+ mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver =
+ BindFakeFrameWidgetInterfaces(main_test_rfh());
+ DragCaptureFrameWidget mock_frame_widget(std::move(receiver));
+
+ // Prepare drag data with files: one with display_name and one without.
+ DropData drop_data;
+ drop_data.filenames.emplace_back(
+ base::FilePath(FILE_PATH_LITERAL("/absolute/path/to/file1.txt")),
+ base::FilePath(FILE_PATH_LITERAL("display_name.txt")));
+ drop_data.filenames.emplace_back(
+ base::FilePath(FILE_PATH_LITERAL("/another/absolute/path/to/file2.txt")),
+ base::FilePath());
+
+ // Call DragTargetDragEnter.
+ base::RunLoop run_loop;
+ GetRenderWidgetHost()->DragTargetDragEnter(
+ drop_data, gfx::PointF(), gfx::PointF(),
+ blink::DragOperationsMask::kDragOperationEvery, 0,
+ base::BindOnce(
+ [](base::OnceClosure quit_closure, ui::mojom::DragOperation operation,
+ bool document_is_handling_drag) { std::move(quit_closure).Run(); },
+ run_loop.QuitClosure()));
+ run_loop.Run();
+
+ // Verify that the paths sent to renderer are sanitized to BaseName.
+ const auto& captured_drag_data = mock_frame_widget.drag_data();
+ ASSERT_TRUE(captured_drag_data);
+ ASSERT_EQ(captured_drag_data->items.size(), 2u);
+
+ const auto& item1 = captured_drag_data->items[0];
+ ASSERT_TRUE(item1->is_file());
+ EXPECT_EQ(item1->get_file()->path,
+ base::FilePath(FILE_PATH_LITERAL("file1.txt")));
+ EXPECT_EQ(item1->get_file()->display_name,
+ base::FilePath(FILE_PATH_LITERAL("display_name.txt")));
+
+ const auto& item2 = captured_drag_data->items[1];
+ ASSERT_TRUE(item2->is_file());
+ EXPECT_EQ(item2->get_file()->path,
+ base::FilePath(FILE_PATH_LITERAL("file2.txt")));
+ EXPECT_TRUE(item2->get_file()->display_name.empty());
+}
+
// A plain <img> drag on macOS populates `file_contents` but supplies neither a
// source URL nor a Content-Disposition, so it must not surface as a File in the
// renderer's DataTransfer.files. See crbug.com/522179938.