Low chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in DevTools
DescriptionIncorrect authorization in DevTools
ComponentDevTools
Bug ClassAuthorization Error
Tracker513821237
Fix commitb0b547bd0179 (devtools/devtools-frontend) +634/-119
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
front_end/models/ai_assistance/agents/NetworkAgent.test.ts
modified
it
front_end/models/ai_assistance/agents/NetworkAgent.test.ts
modified
it
front_end/models/ai_assistance/contexts/RequestContext.test.ts
modified

Files Changed

  • front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts
  • front_end/models/ai_assistance/contexts/RequestContext.ts
  • front_end/models/ai_assistance/agents/NetworkAgent.test.ts
  • front_end/models/ai_assistance/README.md
  • front_end/models/ai_assistance/data_formatters/FileFormatter.ts
  • front_end/models/ai_assistance/tools/GetNetworkRequestDetails.ts
  • front_end/models/ai_assistance/contexts/RequestContext.test.ts
  • front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.test.ts
From b0b547bd0179fe87036b6d802e2c968ea8f3dcd9 Mon Sep 17 00:00:00 2001
From: Jack Franklin <[email protected]>
Date: Mon, 07 Sep 2026 12:41:14 +0000
Subject: [PATCH] AI Assistance: Gate formatter responses with NetworkRequestAccess

Under the Same-Origin Policy, documents cannot inspect cross-origin
network response bodies or unexposed headers without CORS authorization.
DevTools AI Assistance previously included the full response body and
all response headers in prompt and context data sent to remote models,
enabling potential cross-origin data exfiltration via AI prompts.

Changes in this CL:
- Integrate NetworkRequestAccess into NetworkRequestFormatter to evaluate
  response access modes against the initiator security origin.
- Redact response body content for opaque cross-origin requests.
- Restrict response headers to CORS-safelisted or explicitly exposed
  headers for cross-origin requests.
- Update RequestContext to forward the request's initiator security
  origin to the formatter.

Note: Tool-level origin checks in ListNetworkRequests and GetNetworkRequestDetails
are migrated to SecurityOrigin.isSameOriginWith() in the next CL in this stack.

Bug: 513821237
Change-Id: I70a439f86d1092dd52d13497c550e3805e8f618e
TAG=agy
CONV=743cc63e-beab-4cb0-a5bc-230c159cebf9
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/8366126
Reviewed-by: Simon Zünd <[email protected]>
Commit-Queue: Jack Franklin <[email protected]>
Commit-Queue: Simon Zünd <[email protected]>
Auto-Submit: Jack Franklin <[email protected]>
---
diff --git a/front_end/models/ai_assistance/README.md b/front_end/models/ai_assistance/README.md
index fdd9b4c..8f7f8f3 100644
--- a/front_end/models/ai_assistance/README.md
+++ b/front_end/models/ai_assistance/README.md
@@ -63,6 +63,8 @@
 - **Opaque Origins**: Any origin classified as opaque (e.g. `data:`, `about:`, `detached` nodes, unparsed `undefined://` or empty origins) is blocked from starting AI assistance (`isOpaqueOrigin()`).
 - **File Isolation**: Local files (`file://`) do not share a single wildcard origin. Instead, the path is appended (e.g. `file:///path/to/file.js`) to treat each local file as its own unique origin. Swapping local files in the same conversation is blocked.
 - **Trace Isolation**: Imported performance trace recordings are isolated using the virtual origin `imported-trace://${domain}`. This ensures that loaded local trace files do not share origins with live pages, preventing potential cross-origin prompt injection attacks when switching contexts.
+- **HAR Isolation**: Imported HAR network recordings use the virtual origin `imported-har://${domain}` to isolate imported network traffic from live pages.
+- **CORS Response Gating**: Network requests initiated cross-origin without valid CORS authorization (`Access-Control-Allow-Origin`) are classified as `OPAQUE_CROSS_ORIGIN`. DevTools redacts the response body and restricts response headers to CORS-safelisted names to prevent cross-origin data disclosure (b/513821237).
 - **Equivalence**: `areOriginsEquivalent()` ensures opaque origins are never equivalent to anything (including themselves), forcing a conversation reset on transitions.
 
 ## AIAgentV2 (Unified Architecture)
diff --git a/front_end/models/ai_assistance/agents/NetworkAgent.test.ts b/front_end/models/ai_assistance/agents/NetworkAgent.test.ts
index 413aa98..6ccb1f4 100644
--- a/front_end/models/ai_assistance/agents/NetworkAgent.test.ts
+++ b/front_end/models/ai_assistance/agents/NetworkAgent.test.ts
@@ -93,7 +93,8 @@
 
     beforeEach(() => {
       selectedNetworkRequest = SDK.NetworkRequest.NetworkRequest.create(
-          'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com`, urlString``, null, null, null);
+          'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com`,
+          urlString`https://www.example.com`, null, null, null);
       selectedNetworkRequest.statusCode = 200;
       selectedNetworkRequest.setRequestHeaders([{name: 'content-type', value: 'bar1'}]);
       selectedNetworkRequest.responseHeaders =
@@ -104,39 +105,48 @@
             new TextUtils.ContentData.ContentData(exampleResponse, false, 'application/json', 'utf-8'));
       };
       const initiatorNetworkRequest = SDK.NetworkRequest.NetworkRequest.create(
-          'requestId' as Protocol.Network.RequestId, urlString`https://www.initiator.com`, urlString``, null, null,
-          null);
+          'requestId' as Protocol.Network.RequestId, urlString`https://www.initiator.com`,
+          urlString`https://www.example.com`, null, null, null);
       const initiatedNetworkRequest1 = SDK.NetworkRequest.NetworkRequest.create(
-          'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/1`, urlString``, null, null,
-          null);
+          'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/1`,
+          urlString`https://www.example.com`, null, null, null);
       const initiatedNetworkRequest2 = SDK.NetworkRequest.NetworkRequest.create(
-          'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/2`, urlString``, null, null,
-          null);
+          'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/2`,
+          urlString`https://www.example.com`, null, null, null);
 
-      sinon.stub(universe.networkLog, 'initiatorGraphForRequest')
-          .withArgs(selectedNetworkRequest)
-          .returns({
+      const initiatorGraphStub = sinon.stub(universe.networkLog, 'initiatorGraphForRequest');
+      initiatorGraphStub.callsFake((req: SDK.NetworkRequest.NetworkRequest) => {
+        if (req === selectedNetworkRequest) {
+          return {
             initiators: new Set([selectedNetworkRequest, initiatorNetworkRequest]),
             initiated: new Map([
               [selectedNetworkRequest, initiatorNetworkRequest],
               [initiatedNetworkRequest1, selectedNetworkRequest],
               [initiatedNetworkRequest2, selectedNetworkRequest],
             ]),
-          })
-          .withArgs(initiatedNetworkRequest1)
-          .returns({
+          };
+        }
+        if (req === initiatedNetworkRequest1) {
+          return {
             initiators: new Set([]),
             initiated: new Map([
               [initiatedNetworkRequest1, selectedNetworkRequest],
             ]),
-          })
-          .withArgs(initiatedNetworkRequest2)
-          .returns({
+          };
+        }
+        if (req === initiatedNetworkRequest2) {
+          return {
             initiators: new Set([]),
             initiated: new Map([
               [initiatedNetworkRequest2, selectedNetworkRequest],
             ]),
-          });
+          };
+        }
+        return {
+          initiators: new Set([req]),
+          initiated: new Map(),
+        };
+      });
 
       calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator();
       calculator.updateBoundaries(selectedNetworkRequest);
@@ -195,5 +205,45 @@
       const historicalCtx = agent.buildRequest({text: ''}, Host.AidaClient.Role.USER).historical_contexts;
       snapshotTester.assert(this, JSON.stringify(historicalCtx, null, 2));
     });
+
+    it('redacts cross-origin response body in request built for AIDA', async function() {
+      const crossOriginRequest = SDK.NetworkRequest.NetworkRequest.create(
+          'crossOriginRequestId' as Protocol.Network.RequestId,
+          urlString`https://victim.com/sensitive-data`,
+          urlString`https://attacker.com/index.html`,
+          null,
+          null,
+          null,
+      );
+      crossOriginRequest.statusCode = 200;
+      crossOriginRequest.responseHeaders = [
+        {name: 'content-type', value: 'application/json'},
+        {name: 'location', value: '/secret-redirect'},
+        {name: 'www-authenticate', value: 'Bearer secret'},
+      ];
+      crossOriginRequest.timing = timingInfo;
+      crossOriginRequest.requestContentData = () => {
+        return Promise.resolve(new TextUtils.ContentData.ContentData('{"secret":"victim-confidential"}', false,
+                                                                     'application/json', 'utf-8'));
+      };
+
+      const agent = new NetworkAgent.NetworkAgent({
+        aidaClient: mockAidaClient([[{
+          explanation: 'This is the answer',
+          metadata: {
+            rpcGlobalId: 123,
+          },
+        }]]),
+      });
+
+      await Array.fromAsync(agent.run('explain this request',
+                                      {selected: new RequestContext.RequestContext(crossOriginRequest, calculator)}));
+
+      const historicalCtx = agent.buildRequest({text: ''}, Host.AidaClient.Role.USER).historical_contexts;
+      const ctxText = JSON.stringify(historicalCtx);
+      assert.include(ctxText, '<redacted cross-origin response body>');
+      assert.notInclude(ctxText, 'victim-confidential');
+      assert.notInclude(ctxText, 'secret-redirect');
+    });
   });
 });
diff --git a/front_end/models/ai_assistance/contexts/RequestContext.test.ts b/front_end/models/ai_assistance/contexts/RequestContext.test.ts
index d96344c..c8e873b 100644
--- a/front_end/models/ai_assistance/contexts/RequestContext.test.ts
+++ b/front_end/models/ai_assistance/contexts/RequestContext.test.ts
@@ -85,4 +85,125 @@
     assert.isNotNull(details);
     snapshotTester.assert(this, JSON.stringify(details, null, 2));
   });
+
+  it('getPromptDetails redacts response body and non-safelisted headers for cross-origin requests', async function() {
+    const request = SDK.NetworkRequest.NetworkRequest.create(
+        'requestId' as Protocol.Network.RequestId,
+        urlString`https://victim.com/sensitive-data`,
+        urlString`https://attacker.com/index.html`,
+        null,
+        null,
+        null,
+    );
+    request.responseHeaders = [
+      {name: 'Content-Type', value: 'application/json'},
+      {name: 'Location', value: '/secret-redirect'},
+      {name: 'WWW-Authenticate', value: 'Bearer realm="secret"'},
+    ];
+    request.setRequestHeaders([{name: 'Accept', value: 'application/json'}]);
+    request.statusCode = 200;
+    request.requestContentData = () => {
+      return Promise.resolve(
+          new TextUtils.ContentData.ContentData('{"secret":"leak"}', false, 'application/json', 'utf-8'));
+    };
+
+    const calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator();
+    const context = new AiAssistance.RequestContext.RequestContext(request, calculator);
+    const promptDetails = await context.getPromptDetails();
+
+    assert.isNotNull(promptDetails);
+    assert.include(promptDetails!, SDK.NetworkRequestAccess.REDACTED_RESPONSE_BODY);
+    assert.notInclude(promptDetails!, '{"secret":"leak"}');
+    assert.notInclude(promptDetails!, 'Location');
+    assert.notInclude(promptDetails!, 'secret-redirect');
+    assert.notInclude(promptDetails!, 'WWW-Authenticate');
+  });
+
+  it('getUserFacingDetails redacts response body and non-safelisted headers for cross-origin requests',
+     async function() {
+       const request = SDK.NetworkRequest.NetworkRequest.create(
+           'requestId' as Protocol.Network.RequestId,
+           urlString`https://victim.com/sensitive-data`,
+           urlString`https://attacker.com/index.html`,
+           null,
+           null,
+           null,
+       );
+       request.responseHeaders = [
+         {name: 'Content-Type', value: 'application/json'},
+         {name: 'Location', value: '/secret-redirect'},
+         {name: 'WWW-Authenticate', value: 'Bearer realm="secret"'},
+       ];
+       request.setRequestHeaders([{name: 'Accept', value: 'application/json'}]);
+       request.statusCode = 200;
+       request.requestContentData = () => {
+         return Promise.resolve(
+             new TextUtils.ContentData.ContentData('{"secret":"leak"}', false, 'application/json', 'utf-8'));
+       };
+
+       const calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator();
+       const context = new AiAssistance.RequestContext.RequestContext(request, calculator);
+       const details = await context.getUserFacingDetails();
+
+       assert.isNotNull(details);
+       const responseDetail = details!.find(d => d.title === 'Response');
+       assert.isDefined(responseDetail);
+       assert.include(responseDetail!.text, SDK.NetworkRequestAccess.REDACTED_RESPONSE_BODY);
+       assert.notInclude(responseDetail!.text, '{"secret":"leak"}');
+       assert.notInclude(responseDetail!.text, 'Location');
+       assert.notInclude(responseDetail!.text, 'secret-redirect');
+       assert.notInclude(responseDetail!.text, 'WWW-Authenticate');
+     });
+
+  it('preserves response body for same-origin requests in an imported HAR', async () => {
+    const request = SDK.NetworkRequest.NetworkRequest.create(
+        'harRequestId' as Protocol.Network.RequestId,
+        urlString`https://example.com/api/users`,
+        urlString`https://example.com/index.html`,
+        null,
+        null,
+        null,
+    );
+    request.setIsImportedHar(true);
+    request.statusCode = 200;
+    request.responseHeaders = [{name: 'Content-Type', value: 'application/json'}];
+    request.requestContentData = () => {
+      return Promise.resolve(
+          new TextUtils.ContentData.ContentData('{"har":"data"}', false, 'application/json', 'utf-8'));
+    };
+
+    const calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator();
+    const context = new AiAssistance.RequestContext.RequestContext(request, calculator);
+    const promptDetails = await context.getPromptDetails();
+
+    assert.isNotNull(promptDetails);
+    assert.include(promptDetails!, '{"har":"data"}');
+    assert.notInclude(promptDetails!, SDK.NetworkRequestAccess.REDACTED_RESPONSE_BODY);
+  });
+
+  it('redacts response body for cross-origin requests in an imported HAR', async () => {
+    const request = SDK.NetworkRequest.NetworkRequest.create(
+        'harRequestId' as Protocol.Network.RequestId,
+        urlString`https://third-party.com/api/data`,
+        urlString`https://example.com/index.html`,
+        null,
+        null,
+        null,
+    );
+    request.setIsImportedHar(true);
+    request.statusCode = 200;
+    request.responseHeaders = [{name: 'Content-Type', value: 'application/json'}];
+    request.requestContentData = () => {
+      return Promise.resolve(
+          new TextUtils.ContentData.ContentData('{"secret":"leak"}', false, 'application/json', 'utf-8'));
+    };
+
+    const calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator();
+    const context = new AiAssistance.RequestContext.RequestContext(request, calculator);
+    const promptDetails = await context.getPromptDetails();
+
+    assert.isNotNull(promptDetails);
+    assert.include(promptDetails!, SDK.NetworkRequestAccess.REDACTED_RESPONSE_BODY);
+    assert.notInclude(promptDetails!, '{"secret":"leak"}');
+  });
 });
diff --git a/front_end/models/ai_assistance/contexts/RequestContext.ts b/front_end/models/ai_assistance/contexts/RequestContext.ts
index 78ec156..098c298 100644
--- a/front_end/models/ai_assistance/contexts/RequestContext.ts
+++ b/front_end/models/ai_assistance/contexts/RequestContext.ts
@@ -2,17 +2,16 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import * as Common from '../../../core/common/common.js';
 import * as i18n from '../../../core/i18n/i18n.js';
-import type * as Platform from '../../../core/platform/platform.js';
 import type * as SDK from '../../../core/sdk/sdk.js';
 import type * as NetworkTimeCalculator from '../../network_time_calculator/network_time_calculator.js';
 import {
   type ContextDetail,
   ConversationContext,
 } from '../agents/AiAgent.js';
-import {extractContextOrigin} from '../AiOrigins.js';
-import {NetworkRequestFormatter} from '../data_formatters/NetworkRequestFormatter.js';
+import {
+  NetworkRequestFormatter,
+} from '../data_formatters/NetworkRequestFormatter.js';
 
 const UIStringsNotTranslate = {
   request: 'Request',
@@ -25,21 +24,13 @@
 const lockedString = i18n.i18n.lockedString;
 
 /**
- * Returns the origin for a network request in the AI context.
+ * Returns the serialized security origin string for a network request context.
  *
- * To prevent cross-origin prompt injection attacks, HAR-imported requests
- * are isolated from live pages. We assign them a virtual origin
- * (`imported-har://${domain}`) so they do not share the origin of live pages
- * (e.g., `https://${domain}`). This forces a conversation reset when transitioning
- * between imported HAR data and live pages.
+ * @param request The network request from which to extract the context origin.
+ * @returns The resolved security origin string.
  */
 export function getRequestContextOrigin(request: SDK.NetworkRequest.NetworkRequest): string {
-  const origin = extractContextOrigin(request.documentURL);
-  if (request.isImportedHar()) {
-    const parsed = Common.ParsedURL.ParsedURL.fromString(origin as Platform.DevToolsPath.UrlString);
-    return `imported-har://${parsed ? parsed.domain() : origin}`;
-  }
-  return origin;
+  return request.initiatorSecurityOrigin().siteId();
 }
 
 export class RequestContext extends ConversationContext<SDK.NetworkRequest.NetworkRequest> {
@@ -77,13 +68,19 @@
     return this.#request.name();
   }
 
+  #createFormatter(): NetworkRequestFormatter {
+    return new NetworkRequestFormatter(this.#request, this.#calculator, {
+      initiatorSecurityOrigin: this.#request.initiatorSecurityOrigin(),
+    });
+  }
+
   override async getPromptDetails(): Promise<string|null> {
-    const formatter = new NetworkRequestFormatter(this.#request, this.#calculator);
+    const formatter = this.#createFormatter();
     return `# Selected network request\n${await formatter.formatNetworkRequest()}`;
   }
 
   override async getUserFacingDetails(): Promise<[ContextDetail, ...ContextDetail[]]|null> {
-    const formatter = new NetworkRequestFormatter(this.#request, this.#calculator);
+    const formatter = this.#createFormatter();
     const requestContextDetail: ContextDetail = {
       title: lockedString(UIStringsNotTranslate.request),
       text: lockedString(UIStringsNotTranslate.requestUrl) + ': ' + this.#request.url() + '\n\n' +
diff --git a/front_end/models/ai_assistance/data_formatters/FileFormatter.ts b/front_end/models/ai_assistance/data_formatters/FileFormatter.ts
index bee620b..e848d84 100644
--- a/front_end/models/ai_assistance/data_formatters/FileFormatter.ts
+++ b/front_end/models/ai_assistance/data_formatters/FileFormatter.ts
@@ -78,7 +78,9 @@
       const calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator();
       calculator.updateBoundaries(resource.request);
       lines.push(`Request initiator chain:
-${new NetworkRequestFormatter(resource.request, calculator, this.#networkLog).formatRequestInitiatorChain()}`);
+${new NetworkRequestFormatter(resource.request, calculator, {
+        networkLog: this.#networkLog,
+      }).formatRequestInitiatorChain()}`);
     }
     lines.push(`File content:
 ${this.#formatFileContent()}`);
diff --git a/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.test.ts b/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.test.ts
index 6e7ff89..171c9b35 100644
--- a/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.test.ts
+++ b/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.test.ts
@@ -9,6 +9,7 @@
 import * as SDK from '../../../core/sdk/sdk.js';
 import * as TextUtils from '../../../core/text_utils/text_utils.js';
 import * as Protocol from '../../../generated/protocol.js';
+import * as NetworkTimeCalculator from '../../network_time_calculator/network_time_calculator.js';
 import {NetworkRequestFormatter} from '../ai_assistance.js';
 
... (file diff truncated)
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.