Chrome · Serial
CVE-2026-14041
Logic Error in Serial
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ChromeSerialDelegateStoragePartitionTestchrome/browser/serial/chrome_serial_delegate_unittest.cc |
modified |
Files Changed
chrome/browser/serial/chrome_serial_delegate.ccchrome/browser/serial/chrome_serial_delegate.hchrome/browser/serial/chrome_serial_delegate_unittest.cc
Patch
From 16e3c6034cec6e0e2d7d36297c548bf4121611f4 Mon Sep 17 00:00:00 2001 From: Rob Pitkin <[email protected]> Date: Wed, 13 May 2026 13:09:18 -0700 Subject: [PATCH] Fix StoragePartition isolation bypass for Web Serial API. Web Serial permission grants are stored globally in the profile based on origin. While guest views are blocked from using Web Serial, they can open a popup window which inherits the guest's custom StoragePartition but is not detected as a guest frame. This allows the popup to request and persist permission, which then leaks to the rest of the profile. This CL ports the standard mitigation pattern used in ChromeUsbDelegate and ChromeBluetoothDelegate. It blocks Web Serial access for HTTP/HTTPS origins operating in any non-default StoragePartition. A new unit test is added to verify this behavior. Bug: 497544822 Change-Id: I0995e76663e1ccbc9357cb236377eb2996d39de9 Fixed: 497544822 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7833064 Commit-Queue: Rob Pitkin <[email protected]> Reviewed-by: Matt Reynolds <[email protected]> Cr-Commit-Position: refs/heads/main@{#1630154} --- diff --git a/chrome/browser/serial/chrome_serial_delegate.cc b/chrome/browser/serial/chrome_serial_delegate.cc index fb5a078..6eb65757 100644 --- a/chrome/browser/serial/chrome_serial_delegate.cc +++ b/chrome/browser/serial/chrome_serial_delegate.cc @@ -12,7 +12,9 @@ #include "chrome/browser/serial/web_serial_chooser.h" #include "chrome/browser/ui/serial/serial_chooser_controller.h" #include "components/guest_view/buildflags/buildflags.h" +#include "content/public/browser/browser_context.h" #include "content/public/browser/render_frame_host.h" +#include "content/public/browser/storage_partition.h" #include "extensions/buildflags/buildflags.h" #if BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW) @@ -32,6 +34,24 @@ ChromeSerialDelegate::~ChromeSerialDelegate() = default; +bool ChromeSerialDelegate::MayUseSerial(content::RenderFrameHost* frame) { +#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW) + // <webview> and <controlledframe> can not isolate origin-based permissions + // from the rest of profile, therefore serial is disabled inside. + if (extensions::WebViewGuest::FromRenderFrameHost(frame)) { + return false; + } +#endif // BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW) + + content::RenderFrameHost* main_rfh = frame->GetMainFrame(); + if (main_rfh->GetStoragePartition() != + main_rfh->GetBrowserContext()->GetDefaultStoragePartition()) { + return !main_rfh->GetLastCommittedURL().SchemeIsHTTPOrHTTPS(); + } + + return true; +} + std::unique_ptr<content::SerialChooser> ChromeSerialDelegate::RunChooser( content::RenderFrameHost* frame, std::vector<blink::mojom::SerialPortFilterPtr> filters, @@ -46,13 +66,10 @@ bool ChromeSerialDelegate::CanRequestPortPermission( content::RenderFrameHost* frame) { -#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW) - // <webview> and <controlledframe> can not isolate origin-based permissions - // from the rest of profile, therefore serial is disabled inside. - if (extensions::WebViewGuest::FromRenderFrameHost(frame)) { + if (!MayUseSerial(frame)) { return false; } -#endif // BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW) + return GetChooserContext(frame)->CanRequestObjectPermission( frame->GetMainFrame()->GetLastCommittedOrigin()); } @@ -60,13 +77,10 @@ bool ChromeSerialDelegate::HasPortPermission( content::RenderFrameHost* frame, const device::mojom::SerialPortInfo& port) { -#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW) - // <webview> and <controlledframe> can not isolate origin-based permissions - // from the rest of profile, therefore serial is disabled inside. - if (extensions::WebViewGuest::FromRenderFrameHost(frame)) { + if (!MayUseSerial(frame)) { return false; } -#endif // BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW) + return GetChooserContext(frame)->HasPortPermission( frame->GetMainFrame()->GetLastCommittedOrigin(), port); } diff --git a/chrome/browser/serial/chrome_serial_delegate.h b/chrome/browser/serial/chrome_serial_delegate.h index 95e8f82..25d8c5b3 100644 --- a/chrome/browser/serial/chrome_serial_delegate.h +++ b/chrome/browser/serial/chrome_serial_delegate.h @@ -39,6 +39,9 @@ Observer* observer) override; void RemoveObserver(content::RenderFrameHost* frame, Observer* observer) override; + + private: + bool MayUseSerial(content::RenderFrameHost* frame); }; #endif // CHROME_BROWSER_SERIAL_CHROME_SERIAL_DELEGATE_H_ diff --git a/chrome/browser/serial/chrome_serial_delegate_unittest.cc b/chrome/browser/serial/chrome_serial_delegate_unittest.cc new file mode 100644 index 0000000..d933772 --- /dev/null +++ b/chrome/browser/serial/chrome_serial_delegate_unittest.cc @@ -0,0 +1,83 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/serial/chrome_serial_delegate.h" + +#include <memory> + +#include "base/files/file_path.h" +#include "base/unguessable_token.h" +#include "chrome/browser/serial/serial_chooser_context.h" +#include "chrome/browser/serial/serial_chooser_context_factory.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/site_instance.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/storage_partition_config.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/web_contents_tester.h" +#include "services/device/public/mojom/serial.mojom.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace { + +class ChromeSerialDelegateStoragePartitionTest + : public ChromeRenderViewHostTestHarness { + public: + std::unique_ptr<content::WebContents> CreateGuestPartitionWebContents( + const GURL& url) { + const content::StoragePartitionConfig kGuestConfig = + content::StoragePartitionConfig::Create( + profile(), "test_partition", "guest_partition", /*in_memory=*/true); + scoped_refptr<content::SiteInstance> guest_instance = + content::SiteInstance::CreateForGuest(profile(), kGuestConfig); + std::unique_ptr<content::WebContents> guest_contents = + content::WebContentsTester::CreateTestWebContents(profile(), + guest_instance); + content::WebContentsTester::For(guest_contents.get()) + ->NavigateAndCommit(url); + return guest_contents; + } +}; + +TEST_F(ChromeSerialDelegateStoragePartitionTest, + SerialBlocksPermissionAcrossStoragePartition) { + const GURL kGuestUrl("https://example.com/"); + const url::Origin kOrigin = url::Origin::Create(kGuestUrl); + + std::unique_ptr<content::WebContents> guest = + CreateGuestPartitionWebContents(kGuestUrl); + content::RenderFrameHost* rfh = guest->GetPrimaryMainFrame(); + + ASSERT_NE(rfh->GetStoragePartition(), + rfh->GetBrowserContext()->GetDefaultStoragePartition()); + ASSERT_TRUE(rfh->GetLastCommittedURL().SchemeIsHTTPOrHTTPS()); + ASSERT_EQ(kOrigin, rfh->GetLastCommittedOrigin()); + + // Serial should block. + ChromeSerialDelegate serial_delegate; + + EXPECT_FALSE(serial_delegate.CanRequestPortPermission(rfh)) + << "Serial should block permission requests from non-default-partition " + "HTTPS frames"; + + device::mojom::SerialPortInfo port; + port.token = base::UnguessableToken::Create(); + port.path = base::FilePath(FILE_PATH_LITERAL("/dev/ttyPOC0")); + port.display_name = "POC Port"; + + SerialChooserContext* chooser_context = + SerialChooserContextFactory::GetForProfile(profile()); + ASSERT_TRUE(chooser_context); + chooser_context->GrantPortPermission(kOrigin, port); + + EXPECT_FALSE(serial_delegate.HasPortPermission(rfh, port)) + << "Serial should block profile-wide grants leaking into " + "non-default-partition frames"; +} + +} // namespace
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/serial/chrome_serial_delegate_unittest.cc b/chrome/browser/serial/chrome_serial_delegate_unittest.cc
new file mode 100644
index 0000000..d933772
--- /dev/null
+++ b/chrome/browser/serial/chrome_serial_delegate_unittest.cc
@@ -0,0 +1,83 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/serial/chrome_serial_delegate.h"
+
+#include <memory>
+
+#include "base/files/file_path.h"
+#include "base/unguessable_token.h"
+#include "chrome/browser/serial/serial_chooser_context.h"
+#include "chrome/browser/serial/serial_chooser_context_factory.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/site_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/storage_partition_config.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/web_contents_tester.h"
+#include "services/device/public/mojom/serial.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace {
+
+class ChromeSerialDelegateStoragePartitionTest
+ : public ChromeRenderViewHostTestHarness {
+ public:
+ std::unique_ptr<content::WebContents> CreateGuestPartitionWebContents(
+ const GURL& url) {
+ const content::StoragePartitionConfig kGuestConfig =
+ content::StoragePartitionConfig::Create(
+ profile(), "test_partition", "guest_partition", /*in_memory=*/true);
+ scoped_refptr<content::SiteInstance> guest_instance =
+ content::SiteInstance::CreateForGuest(profile(), kGuestConfig);
+ std::unique_ptr<content::WebContents> guest_contents =
+ content::WebContentsTester::CreateTestWebContents(profile(),
+ guest_instance);
+ content::WebContentsTester::For(guest_contents.get())
+ ->NavigateAndCommit(url);
+ return guest_contents;
+ }
+};
+
+TEST_F(ChromeSerialDelegateStoragePartitionTest,
+ SerialBlocksPermissionAcrossStoragePartition) {
+ const GURL kGuestUrl("https://example.com/");
+ const url::Origin kOrigin = url::Origin::Create(kGuestUrl);
+
+ std::unique_ptr<content::WebContents> guest =
+ CreateGuestPartitionWebContents(kGuestUrl);
+ content::RenderFrameHost* rfh = guest->GetPrimaryMainFrame();
+
+ ASSERT_NE(rfh->GetStoragePartition(),
+ rfh->GetBrowserContext()->GetDefaultStoragePartition());
+ ASSERT_TRUE(rfh->GetLastCommittedURL().SchemeIsHTTPOrHTTPS());
+ ASSERT_EQ(kOrigin, rfh->GetLastCommittedOrigin());
+
+ // Serial should block.
+ ChromeSerialDelegate serial_delegate;
+
+ EXPECT_FALSE(serial_delegate.CanRequestPortPermission(rfh))
+ << "Serial should block permission requests from non-default-partition "
+ "HTTPS frames";
+
+ device::mojom::SerialPortInfo port;
+ port.token = base::UnguessableToken::Create();
+ port.path = base::FilePath(FILE_PATH_LITERAL("/dev/ttyPOC0"));
+ port.display_name = "POC Port";
+
+ SerialChooserContext* chooser_context =
+ SerialChooserContextFactory::GetForProfile(profile());
+ ASSERT_TRUE(chooser_context);
+ chooser_context->GrantPortPermission(kOrigin, port);
+
+ EXPECT_FALSE(serial_delegate.HasPortPermission(rfh, port))
+ << "Serial should block profile-wide grants leaking into "
+ "non-default-partition frames";
+}
+
+} // namespace
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index dc53116..ef61953 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -6838,6 +6838,7 @@
"../browser/search_engines/template_url_parser_unittest.cc",
"../browser/search_engines/template_url_service_sync_unittest.cc",
"../browser/search_engines/template_url_service_unittest.cc",
+ "../browser/serial/chrome_serial_delegate_unittest.cc",
"../browser/services_unittest.cc",
"../browser/sessions/chrome_serialized_navigation_driver_unittest.cc",
"../browser/sessions/session_common_utils_unittest.cc",
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page