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 <robpitkin@google.com>
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 <robpitkin@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
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