CVE-2026-10980
Overview
d025eb48e8fe), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug. No root-cause analysis is published for this CVE.Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTcontent/browser/devtools/devtools_session_unittest.cc |
modified |
Files Changed
content/browser/devtools/devtools_session.cccontent/browser/devtools/devtools_session.hcontent/browser/devtools/devtools_session_unittest.cccontent/test/BUILD.gn
Patch
diff --git a/content/browser/devtools/devtools_session.cc b/content/browser/devtools/devtools_session.cc
index fdc9b0f..5368836 100644
--- a/content/browser/devtools/devtools_session.cc
+++ b/content/browser/devtools/devtools_session.cc
@@ -551,9 +551,11 @@
DevToolsAgentHostClient* client,
DevToolsAgentHostImpl* agent_host,
blink::mojom::DevToolsMessagePtr message,
- const std::string& session_id) {
+ const std::string& session_id,
+ const bool& is_notification) {
base::span<const uint8_t> message_span = message->data;
- if (!ValidateSessionId(session_id, message_span)) {
+ if (!ValidateMessage(session_id, /*expected_has_id=*/!is_notification,
+ message_span)) {
if (RenderProcessHost* process_host = agent_host->GetProcessHost()) {
bad_message::ReceivedBadMessage(
process_host, bad_message::RFH_INCONSISTENT_DEVTOOLS_MESSAGE);
@@ -579,7 +581,8 @@
pending_messages_.erase(it->second);
waiting_for_response_.erase(it);
DispatchProtocolResponseOrNotification(client_, agent_host_,
- std::move(message), session_id_);
+ std::move(message), session_id_,
+ /*is_notification=*/false);
// |this| may be deleted at this point.
}
@@ -588,7 +591,8 @@
blink::mojom::RendererOriginatingSessionStatePtr updates) {
ApplySessionStateUpdates(std::move(updates));
DispatchProtocolResponseOrNotification(client_, agent_host_,
- std::move(message), session_id_);
+ std::move(message), session_id_,
+ /*is_notification=*/true);
// |this| may be deleted at this point.
}
@@ -744,8 +748,9 @@
}
// static
-bool DevToolsSession::ValidateSessionId(const std::string& expected_session_id,
- base::span<const uint8_t> message) {
+bool DevToolsSession::ValidateMessage(const std::string& expected_session_id,
+ const bool expected_has_id,
+ base::span<const uint8_t> message) {
std::vector<uint8_t> cbor_message;
crdtp::span<uint8_t> span_message = crdtp::SpanFrom(message);
@@ -762,6 +767,12 @@
crdtp::cbor::GetString8ValueFromMap(span_message,
crdtp::SpanFrom("sessionId"));
+ if (!expected_has_id &&
+ crdtp::cbor::HasKeyInMap(span_message, crdtp::SpanFrom("id"))) {
+ DLOG(ERROR) << "Expected no id in the message but received one";
+ return false;
+ }
+
if (expected_session_id.empty()) {
if (!extracted_session_id.empty()) {
DLOG(ERROR) << "Root session expected no sessionId but received one: "
diff --git a/content/browser/devtools/devtools_session.h b/content/browser/devtools/devtools_session.h
index 31eb8a5..4a04120 100644
--- a/content/browser/devtools/devtools_session.h
+++ b/content/browser/devtools/devtools_session.h
@@ -17,6 +17,7 @@
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "content/browser/devtools/protocol/protocol.h"
+#include "content/common/content_export.h"
#include "content/public/browser/devtools_agent_host_client_channel.h"
#include "content/public/browser/devtools_external_agent_proxy.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
@@ -60,6 +61,10 @@
public content::DevToolsAgentHostClientChannel {
public:
static int GetRootSessionCount();
+ static CONTENT_EXPORT bool ValidateMessage(
+ const std::string& expected_session_id,
+ const bool expected_has_id,
+ base::span<const uint8_t> message);
// For sessions attached to the Tab target, the mode is set to TabTarget.
// For other sessions, the mode is inherited from the parent.
@@ -130,8 +135,6 @@
base::OnceClosure resume_callback);
void DetachChildSession(const std::string& session_id);
bool HasChildSession(const std::string& session_id);
- static bool ValidateSessionId(const std::string& expected_session_id,
- base::span<const uint8_t> message);
DevToolsSession* GetSessionById(const std::string& session_id);
Mode session_mode() const { return mode_; }
@@ -227,7 +230,8 @@
DevToolsAgentHostClient* client,
DevToolsAgentHostImpl* agent_host,
blink::mojom::DevToolsMessagePtr message,
- const std::string& session_id);
+ const std::string& session_id,
+ const bool& is_notification);
// Merges the |updates| received from the renderer into session_state_cookie_.
void ApplySessionStateUpdates(
diff --git a/content/browser/devtools/devtools_session_unittest.cc b/content/browser/devtools/devtools_session_unittest.cc
new file mode 100644
index 0000000..59c6465
--- /dev/null
+++ b/content/browser/devtools/devtools_session_unittest.cc
@@ -0,0 +1,132 @@
+// 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 "content/browser/devtools/devtools_session.h"
+
+#include <string>
+#include <vector>
+
+#include "base/containers/span.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/inspector_protocol/crdtp/cbor.h"
+#include "third_party/inspector_protocol/crdtp/json.h"
+#include "third_party/inspector_protocol/crdtp/span.h"
+
+namespace content {
+namespace {
+
+std::vector<uint8_t> ToVector(const std::string& str) {
+ return std::vector<uint8_t>(str.begin(), str.end());
+}
+
+std::vector<uint8_t> JsonToCbor(const std::string& json) {
+ std::vector<uint8_t> cbor;
+ crdtp::Status status =
+ crdtp::json::ConvertJSONToCBOR(crdtp::SpanFrom(json), &cbor);
+ EXPECT_TRUE(status.ok()) << status.ToASCIIString();
+ return cbor;
+}
+
+TEST(DevToolsSessionValidateMessageTest, MalformedJson) {
+ std::vector<uint8_t> message = ToVector("{invalid cbor/json");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, EmptyMessage) {
+ std::vector<uint8_t> message;
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ValidJson_RootSession_NoIdExpected_Valid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"method\": \"Page.loadEventFired\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ValidCbor_RootSession_NoIdExpected_Valid) {
+ std::vector<uint8_t> message =
+ JsonToCbor("{\"method\": \"Page.loadEventFired\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ RootSession_NoIdExpected_HasId_Invalid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"method\": \"Page.loadEventFired\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_HasId_Valid) {
+ std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_NoId_Valid) {
+ // ValidateMessage does not enforce 'id' presence when expected_has_id is
+ // true.
+ std::vector<uint8_t> message = ToVector("{\"result\": {}}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, RootSession_HasSessionId_Invalid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"sessionId\": \"123\", \"result\": {}}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_ValidMatchingSessionId_NoIdExpected) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"method\": \"Page.loadEventFired\", \"sessionId\": \"session1\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_ValidMatchingSessionId_IdExpected) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_MissingSessionId_Invalid) {
+ std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_MismatchingSessionId_Invalid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session2\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_NoIdExpected_HasId_Invalid) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"id\": 1, \"method\": \"Page.loadEventFired\", \"sessionId\": "
+ "\"session1\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_DuplicateSessionId_Invalid) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
+ "\"session1\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_DuplicateMismatchingSessionId_Invalid) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
+ "\"session2\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+} // namespace
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 2064568d..5e4048f 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2683,6 +2683,7 @@
"../browser/devtools/devtools_background_services_context_impl_unittest.cc",
"../browser/devtools/devtools_frontend_host_impl_unittest.cc",
"../browser/devtools/devtools_http_handler_unittest.cc",
+ "../browser/devtools/devtools_session_unittest.cc",
"../browser/devtools/devtools_video_consumer_unittest.cc",
"../browser/devtools/protocol/tracing_handler_unittest.cc",
"../browser/devtools/protocol_unittest.cc",
Regression Test / PoC
diff --git a/content/browser/devtools/devtools_session_unittest.cc b/content/browser/devtools/devtools_session_unittest.cc
new file mode 100644
index 0000000..59c6465
--- /dev/null
+++ b/content/browser/devtools/devtools_session_unittest.cc
@@ -0,0 +1,132 @@
+// 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 "content/browser/devtools/devtools_session.h"
+
+#include <string>
+#include <vector>
+
+#include "base/containers/span.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/inspector_protocol/crdtp/cbor.h"
+#include "third_party/inspector_protocol/crdtp/json.h"
+#include "third_party/inspector_protocol/crdtp/span.h"
+
+namespace content {
+namespace {
+
+std::vector<uint8_t> ToVector(const std::string& str) {
+ return std::vector<uint8_t>(str.begin(), str.end());
+}
+
+std::vector<uint8_t> JsonToCbor(const std::string& json) {
+ std::vector<uint8_t> cbor;
+ crdtp::Status status =
+ crdtp::json::ConvertJSONToCBOR(crdtp::SpanFrom(json), &cbor);
+ EXPECT_TRUE(status.ok()) << status.ToASCIIString();
+ return cbor;
+}
+
+TEST(DevToolsSessionValidateMessageTest, MalformedJson) {
+ std::vector<uint8_t> message = ToVector("{invalid cbor/json");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, EmptyMessage) {
+ std::vector<uint8_t> message;
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ValidJson_RootSession_NoIdExpected_Valid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"method\": \"Page.loadEventFired\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ValidCbor_RootSession_NoIdExpected_Valid) {
+ std::vector<uint8_t> message =
+ JsonToCbor("{\"method\": \"Page.loadEventFired\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ RootSession_NoIdExpected_HasId_Invalid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"method\": \"Page.loadEventFired\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_HasId_Valid) {
+ std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, RootSession_IdExpected_NoId_Valid) {
+ // ValidateMessage does not enforce 'id' presence when expected_has_id is
+ // true.
+ std::vector<uint8_t> message = ToVector("{\"result\": {}}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest, RootSession_HasSessionId_Invalid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"sessionId\": \"123\", \"result\": {}}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_ValidMatchingSessionId_NoIdExpected) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"method\": \"Page.loadEventFired\", \"sessionId\": \"session1\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_ValidMatchingSessionId_IdExpected) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\"}");
+ EXPECT_TRUE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_MissingSessionId_Invalid) {
+ std::vector<uint8_t> message = ToVector("{\"id\": 1, \"result\": {}}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_MismatchingSessionId_Invalid) {
+ std::vector<uint8_t> message =
+ ToVector("{\"id\": 1, \"result\": {}, \"sessionId\": \"session2\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_NoIdExpected_HasId_Invalid) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"id\": 1, \"method\": \"Page.loadEventFired\", \"sessionId\": "
+ "\"session1\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", false, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_DuplicateSessionId_Invalid) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
+ "\"session1\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+TEST(DevToolsSessionValidateMessageTest,
+ ChildSession_DuplicateMismatchingSessionId_Invalid) {
+ std::vector<uint8_t> message = ToVector(
+ "{\"id\": 1, \"result\": {}, \"sessionId\": \"session1\", \"sessionId\": "
+ "\"session2\"}");
+ EXPECT_FALSE(DevToolsSession::ValidateMessage("session1", true, message));
+}
+
+} // namespace
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 2064568d..5e4048f 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2683,6 +2683,7 @@
"../browser/devtools/devtools_background_services_context_impl_unittest.cc",
"../browser/devtools/devtools_frontend_host_impl_unittest.cc",
"../browser/devtools/devtools_http_handler_unittest.cc",
+ "../browser/devtools/devtools_session_unittest.cc",
"../browser/devtools/devtools_video_consumer_unittest.cc",
"../browser/devtools/protocol/tracing_handler_unittest.cc",
"../browser/devtools/protocol_unittest.cc",
Original Bug Report
CDP response spoofing across DevTools sessions via compromised renderer
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 logic flaw in the DevTools frontend’s message handling combined with insufficient validation in the browser allows a compromised renderer to spoof Chrome DevTools Protocol (CDP) responses for other sessions. By predicting shared message IDs, an attacker can inject malicious data into the DevTools UI for victim origins.
Affected files:
third_party/devtools-frontend/src/front_end/core/protocol_client/DevToolsCDPConnection.tscontent/browser/devtools/devtools_session.ccthird_party/devtools-frontend/src/front_end/core/protocol_client/InspectorBackend.tsthird_party/devtools-frontend/src/front_end/core/sdk/Connections.tschrome/browser/devtools/devtools_ui_bindings.cc
Estimated timestamp from git blame: 2025-10-31
Summary
A potential vulnerability exists in the Chrome DevTools protocol handling that allows a compromised renderer process (Session B) to spoof protocol responses for a different inspected session (Session A) managed by the same DevTools window. This is caused by a lack of session ID verification in the frontend’s command-response matching logic and a failure in the browser process to filter restricted fields in renderer-originated notifications.
Technical Details
1. Frontend: Lack of Session Validation in DevToolsCDPConnection
In the DevTools frontend, the DevToolsCDPConnection class manages CDP communication. It uses a single internal map #callbacks and a shared counter #lastMessageId for all targets multiplexed over the connection (e.g., the main page and cross-origin iframes).
When a command is sent, the callback metadata includes the sessionId, but when a message is received in onMessage, the frontend only uses the id field to retrieve and resolve the callback:
// third_party/devtools-frontend/src/front_end/core/protocol_client/DevToolsCDPConnection.ts
private onMessage(message: string|Object): void {
// ... parsing ...
if ('id' in messageObject && messageObject.id !== undefined) {
const callback = this.#callbacks.get(messageObject.id);
this.#callbacks.delete(messageObject.id);
if (!callback) { return; }
// VULNERABILITY: sessionId in messageObject is not compared against callback.sessionId
callback.resolve(messageObject);
}
}
2. Browser: Insufficient Notification Filtering
A compromised renderer can send protocol messages to the frontend via the DispatchProtocolNotification method of the blink.mojom.DevToolsSessionHost interface. The browser’s implementation in DevToolsSession::ValidateSessionId ensures the sessionId in the message matches the renderer’s session, but it does not prohibit the presence of an id field in the message body.
Typically, only responses (which are generated by the browser or a trusted backend) should contain an id. By allowing a renderer to send a notification containing an id field, the browser allows the renderer to mimic a command response.
Potential Attack Scenario
- A user opens DevTools on a page containing a compromised renderer (Session B) and a victim cross-origin iframe (Session A).
- The compromised renderer predicts the next sequential
messageId(e.g.,100) used by the sharedDevToolsCDPConnection. - When the developer performs an action targeting Session A (e.g., viewing source code), the frontend sends a command with
id: 100andsessionId: "SessionA". - The compromised renderer B immediately sends a spoofed notification via Mojo:
{"id": 100, "sessionId": "SessionB", "result": {"scriptSource": "/* MALICIOUS CODE */"}}. - The browser validates that
sessionIdis “SessionB” and forwards the message. - The frontend receives the message, matches
id: 100, and resolves the victim Session A’s request with the attacker’s data, as it fails to verify that the session IDs match.
Impact
- Cross-Origin Content Spoofing: An attacker can replace the source code, network responses, or console data displayed for a victim origin in the DevTools UI.
- Feature Redirection: Malicious responses for commands like
Target.getTargetInfocould potentially redirect privileged DevTools features (e.g., Lighthouse, Recorder) to attacker-controlled targets.
Suggested Remediation
- Frontend: Update
DevToolsCDPConnection.onMessageto verify that thesessionIdin the incoming message matches thesessionIdstored in the callback metadata before resolving. - Browser: In
DevToolsSession::DispatchProtocolNotification, reject or strip any message body that contains anidfield, as notifications should not possess a call ID.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.