Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in DevTools
DescriptionMissing authorization in DevTools
ComponentDevTools
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513395384
Fix commit01087f5f8f92 (v8/v8) +52/-21
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-08-05 (public git)
Disclosed2026-09-08
Patch gap34 days — fix public in source before the advisory

Background

`V8Inspector` client trust level
a per-session classification where a session is either kFullyTrusted (e.g. local DevTools) or untrusted (e.g. a remote/renderer-exposed client), gating which inspector capabilities the session may use.
`V8ProfilerAgentImpl`
the inspector backend that implements the Profiler protocol domain (CPU profiling, precise coverage), created per session and wired into the session’s command dispatcher.
`V8InspectorSessionImpl`
the object representing one inspector session; its constructor instantiates the per-domain agents and Profiler::Dispatcher::wire registers the Profiler methods so protocol commands can reach them.
Missing authorization
a capability being reachable by a client that should not be entitled to it, here the Profiler domain being exposed to sessions that are not kFullyTrusted.

Root Cause Analysis

In V8InspectorSessionImpl’s constructor, m_profilerAgent was created with new V8ProfilerAgentImpl(...) and wired via protocol::Profiler::Dispatcher::wire unconditionally, for every session regardless of m_clientTrustLevel, while the adjacent V8HeapProfilerAgentImpl was already constructed only inside an if (m_clientTrustLevel == V8Inspector::kFullyTrusted) block. This violated the invariant that the Profiler domain must be restricted to fully trusted clients: an untrusted session could invoke Profiler commands (e.g. Profiler.enable) that a less-privileged client should not be able to reach.

The fix moves the m_profilerAgent construction and Profiler::Dispatcher::wire call inside the same kFullyTrusted guard, so untrusted sessions never get a wired Profiler agent — the new test confirms Profiler.enable then returns Called non-existent method ... code: -32601. Because m_profilerAgent is now potentially null, every previously unconditional dereference (restore, disable, triggerPreciseCoverageDeltaUpdate, and the console.profile/console.profileEnd paths in v8-console.cc via session->profilerAgent()) is wrapped in a null check to avoid a null-pointer crash. The supportedDomainsImpl schema list likewise only advertises the Profiler, HeapProfiler, and Schema domains for fully trusted clients now.

The patch only adds an authorization gate plus the null guards that gate makes necessary; it does not change profiling logic itself.

Key insight
The single mistake is that the Profiler agent was instantiated and dispatcher-wired unconditionally while its sibling HeapProfiler agent was correctly trust-gated, leaving the Profiler domain reachable by untrusted sessions; the fix simply relocates the Profiler creation into the existing kFullyTrusted branch and adds null-guards for the now-optional agent.

Attack Path

  1. Obtain an untrusted inspector session Connect to a V8InspectorSessionImpl whose m_clientTrustLevel is not kFullyTrusted (the new test does exactly this with contextGroup.connect(/* isFullyTrusted */ false)).
  2. Reach the wired `Profiler` domain Before the fix the constructor always ran protocol::Profiler::Dispatcher::wire(&m_dispatcher, m_profilerAgent.get()), so the untrusted session could issue Profiler-domain protocol commands such as Profiler.enable that should have been unavailable.
  3. Exercise profiler functionality from the untrusted context With the domain wired, the untrusted client can drive CPU-profiling / precise-coverage operations (and the console.profile/console.profileEnd paths reach session->profilerAgent()), gaining access to profiling capability it was not authorized for.
  4. Downstream escalation (inferred) Any further impact from exercising the Profiler domain as an untrusted client is not demonstrated by this diff; the patch shows only the missing authorization, not a memory-safety sink or a concrete escalation primitive.

Impact Assessment

An attacker holding an untrusted inspector session gains authorization to the Profiler protocol domain that should be reserved for kFullyTrusted clients, in the process hosting the V8 inspector (e.g. the renderer or the context being debugged). The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3. Any escalation past unauthorized access to profiling capability is (inferred) and not established by the patch. The precondition is the ability to open an inspector session that is not kFullyTrusted.

Changed Functions

FunctionChangeNotes
if
src/inspector/v8-inspector-session-impl.cc
modified

Files Changed

  • test/inspector/cpu-profiler/untrusted-session-expected.txt
  • test/inspector/cpu-profiler/untrusted-session.js
  • src/inspector/v8-console.cc
  • src/inspector/v8-inspector-session-impl.cc

Audit Directions

  • Trust-gated sibling agents
    Audit every per-domain agent created in V8InspectorSessionImpl (and other session constructors) to confirm each privileged domain sits inside the kFullyTrusted guard, since here Profiler was left ungated while HeapProfiler beside it was correctly gated.
  • Unconditional agent dereferences
    Search for m_*Agent-> and session->*Agent()-> call sites that assume an agent always exists; once an agent becomes trust-conditional, each dereference (including in v8-console.cc and coverage/restore paths) needs a null guard.
  • Domain advertisement vs. dispatch consistency
    Cross-check supportedDomainsImpl and Dispatcher::wire registrations so a domain that is not advertised is also not reachable via the dispatcher (and vice versa), catching authorization gaps between what a client is told it can use and what commands actually resolve.
From 01087f5f8f921c5f55f8f6bda4132d48f5eba0e7 Mon Sep 17 00:00:00 2001
From: Philip Pfaffe <pfaffe@chromium.org>
Date: Wed, 05 Aug 2026 13:55:02 +0000
Subject: [PATCH] Require fully trusted clients for the inspector profiler

Fixed: 513395384
Change-Id: Icc180358092f354a7e719ae3aad807249b2a95c9
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8203445
Reviewed-by: Simon Zünd <szuend@chromium.org>
Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109069}
---
diff --git a/src/inspector/v8-console.cc b/src/inspector/v8-console.cc
index 88d0da1..0fa7caf 100644
--- a/src/inspector/v8-console.cc
+++ b/src/inspector/v8-console.cc
@@ -389,7 +389,9 @@
   String16 title =
       toProtocolString(m_inspector->isolate(), helper.firstArgToString());
   helper.forEachSession([&title](V8InspectorSessionImpl* session) {
-    session->profilerAgent()->consoleProfile(title);
+    if (session->profilerAgent()) {
+      session->profilerAgent()->consoleProfile(title);
+    }
   });
   TRACE_EVENT_END(TRACE_DISABLED_BY_DEFAULT("v8.inspector"), "title",
                   title.utf8().c_str());
@@ -403,7 +405,9 @@
   String16 title =
       toProtocolString(m_inspector->isolate(), helper.firstArgToString());
   helper.forEachSession([&title](V8InspectorSessionImpl* session) {
-    session->profilerAgent()->consoleProfileEnd(title);
+    if (session->profilerAgent()) {
+      session->profilerAgent()->consoleProfileEnd(title);
+    }
   });
   TRACE_EVENT_END(TRACE_DISABLED_BY_DEFAULT("v8.inspector"), "title",
                   title.utf8().c_str());
diff --git a/src/inspector/v8-inspector-session-impl.cc b/src/inspector/v8-inspector-session-impl.cc
index e7d788d..99271ac 100644
--- a/src/inspector/v8-inspector-session-impl.cc
+++ b/src/inspector/v8-inspector-session-impl.cc
@@ -138,11 +138,11 @@
       this, this, agentState(protocol::Console::Metainfo::domainName)));
   protocol::Console::Dispatcher::wire(&m_dispatcher, m_consoleAgent.get());
 
-  m_profilerAgent.reset(new V8ProfilerAgentImpl(
-      this, this, agentState(protocol::Profiler::Metainfo::domainName)));
-  protocol::Profiler::Dispatcher::wire(&m_dispatcher, m_profilerAgent.get());
-
   if (m_clientTrustLevel == V8Inspector::kFullyTrusted) {
+    m_profilerAgent.reset(new V8ProfilerAgentImpl(
+        this, this, agentState(protocol::Profiler::Metainfo::domainName)));
+    protocol::Profiler::Dispatcher::wire(&m_dispatcher, m_profilerAgent.get());
+
     m_heapProfilerAgent.reset(new V8HeapProfilerAgentImpl(
         this, this, agentState(protocol::HeapProfiler::Metainfo::domainName)));
     protocol::HeapProfiler::Dispatcher::wire(&m_dispatcher,
@@ -156,7 +156,7 @@
     m_runtimeAgent->restore();
     m_debuggerAgent->restore();
     if (m_heapProfilerAgent) m_heapProfilerAgent->restore();
-    m_profilerAgent->restore();
+    if (m_profilerAgent) m_profilerAgent->restore();
     m_consoleAgent->restore();
   }
 }
@@ -165,7 +165,7 @@
   v8::Isolate::Scope scope(m_inspector->isolate());
   discardInjectedScripts();
   m_consoleAgent->disable();
-  m_profilerAgent->disable();
+  if (m_profilerAgent) m_profilerAgent->disable();
   if (m_heapProfilerAgent) m_heapProfilerAgent->disable();
   m_debuggerAgent->disable();
   m_runtimeAgent->disable();
@@ -442,18 +442,20 @@
                        .setName(protocol::Debugger::Metainfo::domainName)
                        .setVersion(protocol::Debugger::Metainfo::version)
                        .build());
-  result.push_back(protocol::Schema::Domain::create()
-                       .setName(protocol::Profiler::Metainfo::domainName)
-                       .setVersion(protocol::Profiler::Metainfo::version)
-                       .build());
-  result.push_back(protocol::Schema::Domain::create()
-                       .setName(protocol::HeapProfiler::Metainfo::domainName)
-                       .setVersion(protocol::HeapProfiler::Metainfo::version)
-                       .build());
-  result.push_back(protocol::Schema::Domain::create()
-                       .setName(protocol::Schema::Metainfo::domainName)
-                       .setVersion(protocol::Schema::Metainfo::version)
-                       .build());
+  if (m_clientTrustLevel == V8Inspector::kFullyTrusted) {
+    result.push_back(protocol::Schema::Domain::create()
+                         .setName(protocol::Profiler::Metainfo::domainName)
+                         .setVersion(protocol::Profiler::Metainfo::version)
+                         .build());
+    result.push_back(protocol::Schema::Domain::create()
+                         .setName(protocol::HeapProfiler::Metainfo::domainName)
+                         .setVersion(protocol::HeapProfiler::Metainfo::version)
+                         .build());
+    result.push_back(protocol::Schema::Domain::create()
+                         .setName(protocol::Schema::Metainfo::domainName)
+                         .setVersion(protocol::Schema::Metainfo::version)
+                         .build());
+  }
   return result;
 }
 
@@ -521,7 +523,9 @@
 
 void V8InspectorSessionImpl::triggerPreciseCoverageDeltaUpdate(
     StringView occasion) {
-  m_profilerAgent->triggerPreciseCoverageDeltaUpdate(toString16(occasion));
+  if (m_profilerAgent) {
+    m_profilerAgent->triggerPreciseCoverageDeltaUpdate(toString16(occasion));
+  }
 }
 
 V8InspectorSession::EvaluateResult V8InspectorSessionImpl::evaluate(
diff --git a/test/inspector/cpu-profiler/untrusted-session-expected.txt b/test/inspector/cpu-profiler/untrusted-session-expected.txt
new file mode 100644
index 0000000..eb29fc0
--- /dev/null
+++ b/test/inspector/cpu-profiler/untrusted-session-expected.txt
@@ -0,0 +1,3 @@
+Tests that Profiler domain is not available in untrusted session.
+console.profile in untrusted session did not crash.
+Error: Called non-existent method. 'Profiler.enable' wasn't found code: -32601
diff --git a/test/inspector/cpu-profiler/untrusted-session.js b/test/inspector/cpu-profiler/untrusted-session.js
new file mode 100644
index 0000000..47c1b2c
--- /dev/null
+++ b/test/inspector/cpu-profiler/untrusted-session.js
@@ -0,0 +1,20 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+InspectorTest.start(
+    'Tests that Profiler domain is not available in untrusted session.');
+
+(async () => {
+  const contextGroup = new InspectorTest.ContextGroup();
+  const session = contextGroup.connect(/* isFullyTrusted */ false);
+  const {Protocol} = session;
+
+  await Protocol.Runtime.evaluate({
+    expression: 'console.profile("test"); console.profileEnd("test");'
+  });
+  InspectorTest.log('console.profile in untrusted session did not crash.');
+  await Protocol.Profiler.enable();
+
+  InspectorTest.completeTest();
+})();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.