Low watch CVSS 4.3 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactConfused deputy in DevTools
DescriptionConfused deputy in DevTools
ComponentDevTools
Sub-componentInspector
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.
Tracker533095855
Fix commitb40ca4aa9abe (v8/v8) +215/-0
CWECWE-441
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-08-28 (public git)
Disclosed2026-09-22
Patch gap25 days — fix public in source before the advisory

Background

Custom formatters
DevTools feature where a page registers devtoolsFormatters whose header/body callbacks return JSONML that the inspector renders as an object preview.
JSONML
the array-based markup ([tag, attributes, ...children]) that custom formatters emit and that substituteObjectTags walks to build a customPreview.
`v8::Value::IsArray()`
a V8 type predicate that returns false for a JSProxy, so a proxy wrapping an array is not recognized as an array by this check.
`v8::Proxy`
a JS Proxy exotic object whose traps run arbitrary user code on property access and whose type predicates diverge from those of its target.

Root Cause Analysis

In substituteObjectTags (src/inspector/custom-preview.cc), the inspector validated JSONML nodes and their attribute dictionaries with plain type predicates such as IsObject() and IsArray(), then treated the value as a trusted object or array. A JS Proxy violates the invariant these checks assume: attributesValue->IsProxy() can be an object that passes the “attributes should be an Object” gate, and value->IsArray() returns false for a proxy wrapping an array, so a proxy child could slip past the recursive array-handling branch while still being consumed as JSONML. Because a proxy’s traps execute arbitrary page-controlled code during subsequent property reads, the formatter effectively became a confused deputy operating on attacker-controlled exotic objects.

The fix adds explicit value->IsProxy() and attributesValue->IsProxy() guards that call reportError and return false, rejecting proxies both as JSONML elements and as attribute dictionaries before any trap can be triggered. This restores the invariant that only genuine objects and arrays flow through the JSONML substitution path.

Key insight
The single mistake was validating JSONML with predicates (IsObject(), IsArray()) that a Proxy can either satisfy deceptively or evade entirely; the fix closes the gap by explicitly detecting IsProxy() and rejecting such values outright rather than trusting the shape checks.

Attack Path

  1. Register a malicious formatter A page sets this.devtoolsFormatters to a formatter whose header or body returns JSONML embedding a Proxy, e.g. ['span', {}, new Proxy([], {})].
  2. Trigger preview generation The page (or DevTools inspecting it) causes an object to be evaluated with generatePreview, invoking the formatter and passing its JSONML into substituteObjectTags.
  3. Bypass validation A proxy-over-array child evades the value->IsArray() branch, or new Proxy({}, {}) passes the attributes-object check, so the proxy is accepted as a valid JSONML element or attribute dictionary.
  4. Trigger proxy traps Subsequent property reads on the accepted proxy run attacker-defined traps within the inspector’s formatting context, letting the page influence DevTools rendering it should not control.

Impact Assessment

An attacker who controls a page can smuggle a Proxy into the DevTools custom-preview pipeline, causing the inspector to act on attacker-controlled exotic objects and run trap code during preview formatting (a confused deputy in DevTools). Exploitation requires the victim to have custom object formatters enabled and to inspect or generate a preview for the attacker’s object; the fix reduces the outcome to a clean Custom Formatter Failed error with no preview generated.

Changed Functions

FunctionChangeNotes
if
test/inspector/runtime/custom-preview-proxy.js
modified

Files Changed

  • test/inspector/runtime/custom-preview-proxy-expected.txt
  • test/inspector/runtime/custom-preview-proxy.js
  • src/inspector/custom-preview.cc

Audit Directions

  • Type predicates vs. proxies
    Audit other inspector/DevTools code that gates on IsObject()/IsArray() and then dereferences; a JSProxy may pass or fail these deceptively and should be explicitly excluded where traps must not run.
  • Recursive JSONML/user-object walkers
    Review any code that recursively descends attacker-supplied object graphs (formatters, previews, serializers) for missing exotic-object rejection at each level, not just the top.
  • Confused-deputy surfaces in DevTools
    Look for validation gates that assume shape checks imply trusted, side-effect-free access, where a proxy could turn a trusted component into an executor of page-controlled code.
From b40ca4aa9abe852c75ebf0afaa348459b4161dd9 Mon Sep 17 00:00:00 2001
From: Simon Zünd <[email protected]>
Date: Fri, 28 Aug 2026 06:09:31 +0000
Subject: [PATCH] [inspector] Disallow proxy values in JSONML for custom formatters

In substituteObjectTags, JS proxies could masquerade as objects or
arrays and bypass validation, since v8::Value::IsArray() returns false
for JSProxy instances and attributes were not checked for proxies.

This CL adds explicit checks for v8::Value::IsProxy() in
substituteObjectTags and reports an error if encountered, ensuring
proxies are rejected both as JSONML elements and as attribute
dictionaries.

TAG=agy CONV=8316c18a-958e-4e9f-b927-e516a24655c4 [email protected]

Fixed: 533095855
Change-Id: Ic2afc2e0f82581169cd876634188e6099d482c98
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8312289
Reviewed-by: Philip Pfaffe <[email protected]>
Commit-Queue: Simon Zünd <[email protected]>
Cr-Commit-Position: refs/heads/main@{#109553}
---
diff --git a/src/inspector/custom-preview.cc b/src/inspector/custom-preview.cc
index 5553068..469705c 100644
--- a/src/inspector/custom-preview.cc
+++ b/src/inspector/custom-preview.cc
@@ -84,6 +84,10 @@
       reportError(context, tryCatch, "attributes should be an Object");
       return false;
     }
+    if (attributesValue->IsProxy()) {
+      reportError(context, tryCatch, "Proxy values are not allowed in JSONML");
+      return false;
+    }
     v8::Local<v8::Object> attributes = attributesValue.As<v8::Object>();
     v8::Local<v8::Value> originValue;
     if (!attributes->Get(context, objectLiteral).ToLocal(&originValue)) {
@@ -146,6 +150,11 @@
         reportError(context, tryCatch);
         return false;
       }
+      if (value->IsProxy()) {
+        reportError(context, tryCatch,
+                    "Proxy values are not allowed in JSONML");
+        return false;
+      }
       if (value->IsArray() && value.As<v8::Array>()->Length() > 0 &&
           !substituteObjectTags(sessionId, groupName, context,
                                 value.As<v8::Array>(), maxDepth - 1)) {
diff --git a/test/inspector/runtime/custom-preview-proxy-expected.txt b/test/inspector/runtime/custom-preview-proxy-expected.txt
new file mode 100644
index 0000000..60cdf68
--- /dev/null
+++ b/test/inspector/runtime/custom-preview-proxy-expected.txt
@@ -0,0 +1,91 @@
+Checks that proxies in JSONML are rejected by custom formatters
+Test proxy child in header JSONML..
+{
+    method : Runtime.consoleAPICalled
+    params : {
+        args : [
+            [0] : {
+                type : string
+                value : Custom Formatter Failed: Uncaught Proxy values are not allowed in JSONML
+            }
+        ]
+        executionContextId : <executionContextId>
+        timestamp : <timestamp>
+        type : error
+    }
+}
+No customPreview generated
+Test proxy attributes in header JSONML..
+{
+    method : Runtime.consoleAPICalled
+    params : {
+        args : [
+            [0] : {
+                type : string
+                value : Custom Formatter Failed: Uncaught Proxy values are not allowed in JSONML
+            }
+        ]
+        executionContextId : <executionContextId>
+        timestamp : <timestamp>
+        type : error
+    }
+}
+No customPreview generated
+Test proxy array child masquerading as tag..
+{
+    method : Runtime.consoleAPICalled
+    params : {
+        args : [
+            [0] : {
+                type : string
+                value : Custom Formatter Failed: Uncaught Proxy values are not allowed in JSONML
+            }
+        ]
+        executionContextId : <executionContextId>
+        timestamp : <timestamp>
+        type : error
+    }
+}
+No customPreview generated
+Test proxy attributes in object tag..
+{
+    method : Runtime.consoleAPICalled
+    params : {
+        args : [
+            [0] : {
+                type : string
+                value : Custom Formatter Failed: Uncaught Proxy values are not allowed in JSONML
+            }
+        ]
+        executionContextId : <executionContextId>
+        timestamp : <timestamp>
+        type : error
+    }
+}
+No customPreview generated
+Test proxy child in body JSONML..
+customPreview: ["span",{},"Header ok"]
+{
+    method : Runtime.consoleAPICalled
+    params : {
+        args : [
+            [0] : {
+                type : string
+                value : Custom Formatter Failed: Uncaught Proxy values are not allowed in JSONML
+            }
+        ]
+        executionContextId : <executionContextId>
+        timestamp : <timestamp>
+        type : error
+    }
+}
+{
+    id : <messageId>
+    result : {
+        result : {
+            type : undefined
+        }
+    }
+}
+Test formatting a proxy object itself (allowed)..
+customPreview: ["span",{},"Proxy formatted: ","proxyObject"]
diff --git a/test/inspector/runtime/custom-preview-proxy.js b/test/inspector/runtime/custom-preview-proxy.js
new file mode 100644
index 0000000..c525d84
--- /dev/null
+++ b/test/inspector/runtime/custom-preview-proxy.js
@@ -0,0 +1,115 @@
+// 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.
+
+const { session, contextGroup, Protocol } =
+  InspectorTest.start('Checks that proxies in JSONML are rejected by custom formatters');
+
+(async function test() {
+  Protocol.Runtime.enable();
+  Protocol.Runtime.setCustomObjectFormatterEnabled({ enabled: true });
+  Protocol.Runtime.onConsoleAPICalled(m => InspectorTest.logMessage(m));
+
+  InspectorTest.log('Test proxy child in header JSONML..');
+  await Protocol.Runtime.evaluate({
+    expression: `
+      this.devtoolsFormatters = [{
+        header: () => ['span', {}, 'Header with proxy child', new Proxy([], {})],
+        hasBody: () => false
+      }];
+    `
+  });
+  let result = await Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
+  dumpCustomPreview(result);
+
+  InspectorTest.log('Test proxy attributes in header JSONML..');
+  await Protocol.Runtime.evaluate({
+    expression: `
+      this.devtoolsFormatters = [{
+        header: () => ['span', new Proxy({}, {}), 'Header with proxy attributes'],
+        hasBody: () => false
+      }];
+    `
+  });
+  result = await Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
+  dumpCustomPreview(result);
+
+  InspectorTest.log('Test proxy array child masquerading as tag..');
+  await Protocol.Runtime.evaluate({
+    expression: `
+      this.devtoolsFormatters = [{
+        header: () => ['span', {}, new Proxy(['object', {object: {}}], {})],
+        hasBody: () => false
+      }];
+    `
+  });
+  result = await Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
+  dumpCustomPreview(result);
+
+  InspectorTest.log('Test proxy attributes in object tag..');
+  await Protocol.Runtime.evaluate({
+    expression: `
+      this.devtoolsFormatters = [{
+        header: () => ['span', {}, ['object', new Proxy({object: {}}, {})]],
+        hasBody: () => false
+      }];
+    `
+  });
+  result = await Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
+  dumpCustomPreview(result);
+
+  InspectorTest.log('Test proxy child in body JSONML..');
+  await Protocol.Runtime.evaluate({
+    expression: `
+      this.devtoolsFormatters = [{
+        header: () => ['span', {}, 'Header ok'],
+        hasBody: () => true,
+        body: () => ['span', {}, 'Body with proxy child', new Proxy([], {})]
+      }];
+    `
+  });
+  result = await Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
+  await dumpCustomPreviewWithBody(result);
+
+  InspectorTest.log('Test formatting a proxy object itself (allowed)..');
+  await Protocol.Runtime.evaluate({
+    expression: `
+      var targetProxy = new Proxy({name: 'proxyObject'}, {});
+      this.devtoolsFormatters = [{
+        header: (x) => x === targetProxy ? ['span', {}, 'Proxy formatted: ', x.name] : null,
+        hasBody: () => false
+      }];
+    `
+  });
+  result = await Protocol.Runtime.evaluate({ expression: 'targetProxy', generatePreview: true });
+  dumpCustomPreview(result);
+
+  InspectorTest.completeTest();
+})();
+
+function dumpCustomPreview(result) {
+  const remoteObject = result.result.result;
+  if (remoteObject.customPreview) {
+    InspectorTest.log('customPreview: ' + remoteObject.customPreview.header);
+  } else {
+    InspectorTest.log('No customPreview generated');
+  }
+}
+
+async function dumpCustomPreviewWithBody(result) {
+  const remoteObject = result.result.result;
+  if (remoteObject.customPreview) {
+    InspectorTest.log('customPreview: ' + remoteObject.customPreview.header);
+    if (remoteObject.customPreview.bodyGetterId) {
+      const body = await Protocol.Runtime.callFunctionOn({
+        objectId: remoteObject.objectId,
+        functionDeclaration: 'function(bodyGetter) { return bodyGetter.call(this); }',
+        arguments: [{ objectId: remoteObject.customPreview.bodyGetterId }],
+        returnByValue: true
+      });
+      InspectorTest.logMessage(body);
+    }
+  } else {
+    InspectorTest.log('No customPreview generated');
+  }
+}
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.