| // 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. |
| |
| import * as Common from '../common/common.js'; |
| import type * as Platform from '../platform/platform.js'; |
| |
| /** |
| * Internal representation of a security origin: |
| * - `origin`: A comparable origin string (`<scheme>://<host>[:<port>]`). |
| * - `file`: A path-scoped local file origin (`file://<host>/<path>`). |
| * - `opaque`: An isolated opaque origin uniquely identified by a UUID that never matches any other origin. |
| */ |
| type InternalOrigin =|{readonly type: 'origin', readonly value: string}|{readonly type: 'file', readonly value: string}| |
| {readonly type: 'opaque', readonly uuid: string}; |
| |
| /** Exact string matches that represent an invalid or opaque origin. */ |
| const OPAQUE_EXACT_MATCHES = new Set([ |
| '', |
| 'null', |
| 'undefined', |
| 'data:', |
| 'detached', |
| ]); |
| |
| /** String prefixes for origins that must be treated as opaque (e.g. `about:blank`, `blob:data`). */ |
| const OPAQUE_PREFIXES = [ |
| 'about:', |
| 'blob:about', |
| 'blob:data', |
| 'blob:null', |
| ] as const; |
| |
| /** |
| * Scheme prefixes for imported artifact data (such as HAR archives and performance traces). |
| * DevTools isolates imported artifacts into scheme-scoped and host-scoped origins |
| * (`imported-har://${host}`, `imported-trace://${host}`). These origins never match live |
| * web origins (`https://${host}`) or other artifact schemes. |
| */ |
| export const IMPORTED_ORIGIN_PREFIXES: ReadonlySet<string> = new Set([ |
| 'imported-har:', |
| 'imported-trace:', |
| ]); |
| |
| function isOpaqueUrlString(url: string): boolean { |
| const lower = url.trim().toLowerCase(); |
| if (OPAQUE_EXACT_MATCHES.has(lower)) { |
| return true; |
| } |
| return OPAQUE_PREFIXES.some(prefix => lower.startsWith(prefix)); |
| } |
| |
| /** |
| * An immutable security origin for an entity in DevTools (such as a document, |
| * network request, or storage key). |
| * |
| * DevTools features that handle user data or execute commands on behalf of the user |
| * (such as DevTools AI Assistance and Extensions) must enforce strict origin boundaries |
| * to prevent prompt injection and cross-origin data exfiltration. |
| * |
| * This class serves as the single source of truth for origin comparison and classification: |
| * |
| * 1. **Standard Origins**: Web URLs (HTTP, HTTPS, WSS) resolve to `<scheme>://<host>[:<port>]`. |
| * Two standard origins are same-origin if their scheme, host, and port match. |
| * |
| * 2. **File URLs (`file://`)**: In the WHATWG web security model, `file://` URLs are assigned |
| * opaque origins. In DevTools, however, users frequently debug local files (`file:///path/index.html`). |
| * Treating all `file://` URLs as opaque would block the user from inspecting elements or styles within |
| * the same file. Conversely, treating all `file://` URLs as a single shared origin would allow a malicious |
| * local file to traverse into other local files via iframes (b/524362513). |
| * Therefore, DevTools treats `file://` URLs as **path-scoped origins** (`file://<authority><path>`). |
| * `isOpaque()` returns `false` for `file://` URLs, and two `file://` URLs are considered same-origin |
| * only if their full file path and host match exactly. |
| * |
| * 3. **Imported Artifact Origins (`imported-har:`, `imported-trace:`)**: Custom schemes for imported |
| * recordings (such as HAR archives and performance traces) resolve to `<scheme>//<host>`. |
| * DevTools isolates imported artifact origins from live web pages (`imported-trace://example.com` != `https://example.com`) |
| * and isolates different artifact schemes from each other (`imported-trace://example.com` != `imported-har://example.com`). |
| * |
| * 4. **Opaque Origins**: Opaque contexts (`data:`, `about:blank`, invalid URLs, or synthetic |
| * opaque origins) are backed by unique UUIDs. An opaque origin never matches any other origin, |
| * even another opaque origin created from the same URL string. |
| */ |
| export class SecurityOrigin { |
| readonly #origin: InternalOrigin; |
| |
| private constructor(origin: InternalOrigin) { |
| this.#origin = origin; |
| } |
| |
| /** |
| * Creates a `SecurityOrigin` instance from a raw URL or origin string. |
| * |
| * - If the URL is determined to be opaque (e.g. `data:`, `about:blank`, empty, `null`), |
| * a new unique opaque origin is returned. |
| * - If the URL is an imported artifact scheme (e.g. `imported-har:`, `imported-trace:`), |
| * a scheme-and-host origin (`<scheme>//<host>`) is returned. |
| * - If the URL is a `file://` URL, a path-scoped origin (`file://<authority><path>`) is returned. |
| * - Otherwise, the standard origin (`<scheme>://<host>[:<port>]`) is extracted and returned. |
| * |
| * @param rawUrl The raw URL or origin string to evaluate. |
| */ |
| static create(rawUrl: string): SecurityOrigin { |
| if (isOpaqueUrlString(rawUrl)) { |
| return SecurityOrigin.createUniqueOpaque(); |
| } |
| |
| const importedOrigin = SecurityOrigin.#tryCreateImportedArtifactOrigin(rawUrl); |
| if (importedOrigin) { |
| return importedOrigin; |
| } |
| |
| if (rawUrl.toLowerCase().startsWith('file://')) { |
| const parsed = Common.ParsedURL.ParsedURL.fromString(rawUrl as Platform.DevToolsPath.UrlString); |
| if (!parsed) { |
| return SecurityOrigin.createUniqueOpaque(); |
| } |
| const authority = parsed.host + (parsed.port ? ':' + parsed.port : ''); |
| return new SecurityOrigin({type: 'file', value: `file://${authority}${parsed.path}`}); |
| } |
| |
| const origin = Common.ParsedURL.ParsedURL.extractOrigin(rawUrl as Platform.DevToolsPath.UrlString); |
| if (!origin || isOpaqueUrlString(origin)) { |
| return SecurityOrigin.createUniqueOpaque(); |
| } |
| |
| return new SecurityOrigin({type: 'origin', value: origin.toLowerCase()}); |
| } |
| |
| /** |
| * Attempts to parse a URL as an imported artifact scheme (such as `imported-har:` or `imported-trace:`). |
| * |
| * Standard web origins do not match imported artifact origins. If the URL starts with an imported |
| * prefix, this helper isolates the origin to `<scheme>//<host>`. If the authority or host is missing, |
| * or if the URL cannot be parsed, it returns a unique opaque origin. |
| * |
| * @param rawUrl The raw URL string to evaluate. |
| * @returns A `SecurityOrigin` if the URL matches an imported artifact scheme, or `null` otherwise. |
| */ |
| static #tryCreateImportedArtifactOrigin(rawUrl: string): SecurityOrigin|null { |
| const lowerUrl = rawUrl.toLowerCase(); |
| for (const prefix of IMPORTED_ORIGIN_PREFIXES) { |
| if (lowerUrl.startsWith(prefix)) { |
| try { |
| const parsedUrl = new URL(rawUrl); |
| if (!parsedUrl.host) { |
| return SecurityOrigin.createUniqueOpaque(); |
| } |
| return new SecurityOrigin({type: 'origin', value: `${parsedUrl.protocol}//${parsedUrl.host.toLowerCase()}`}); |
| } catch { |
| return SecurityOrigin.createUniqueOpaque(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Creates a synthetic, unique opaque origin. |
| * |
| * Useful when an entity (like a sandboxed iframe or detached DOM tree) needs an |
| * isolated origin that will never match any other origin in the session. |
| */ |
| static createUniqueOpaque(): SecurityOrigin { |
| return new SecurityOrigin({type: 'opaque', uuid: crypto.randomUUID()}); |
| } |
| |
| /** |
| * Creates an isolated security origin for an imported performance trace. |
| * |
| * Imported traces isolate to `imported-trace://${authority}` based on the recorded |
| * main frame URL. If the URL is missing, invalid, or has no host, this returns a |
| * unique opaque origin so that unhosted traces do not share access with each other |
| * or live web origins. |
| * |
| * @param mainFrameURL The URL string of the main frame recorded in the trace. |
| */ |
| static createForImportedTrace(mainFrameURL: string|null|undefined): SecurityOrigin { |
| if (!mainFrameURL) { |
| return SecurityOrigin.createUniqueOpaque(); |
| } |
| const parsed = Common.ParsedURL.ParsedURL.fromString(mainFrameURL); |
| if (!parsed?.host) { |
| return SecurityOrigin.createUniqueOpaque(); |
| } |
| const authority = parsed.host + (parsed.port ? `:${parsed.port}` : ''); |
| return SecurityOrigin.create(`imported-trace://${authority}`); |
| } |
| |
| /** |
| * Checks whether this security origin is equivalent to another security origin. |
| * |
| * - Standard origins return `true` if their scheme, host, and port match. |
| * - File origins return `true` if their full file path and host match. |
| * - Opaque origins return `true` only if both instances have identical UUIDs. |
| * - Passing `null` always returns `false`. |
| * |
| * @param other The other `SecurityOrigin` to compare with. |
| */ |
| isSameOriginWith(other: SecurityOrigin|null): boolean { |
| if (!other) { |
| return false; |
| } |
| if (this.#origin.type === 'opaque' || other.#origin.type === 'opaque') { |
| return this.#origin.type === 'opaque' && other.#origin.type === 'opaque' && |
| this.#origin.uuid === other.#origin.uuid; |
| } |
| return this.#origin.type === other.#origin.type && this.#origin.value === other.#origin.value; |
| } |
| |
| /** |
| * Returns whether this origin is opaque. |
| * |
| * Opaque origins include `data:` URLs, `about:blank`, invalid URLs, and instances |
| * created via `createUniqueOpaque()`. |
| */ |
| isOpaque(): boolean { |
| return this.#origin.type === 'opaque'; |
| } |
| |
| /** |
| * Returns whether this origin represents a local file origin (`file://`). |
| */ |
| isFile(): boolean { |
| return this.#origin.type === 'file'; |
| } |
| |
| /** |
| * Returns a stable string identifier for display, logging, or storage keys. |
| * |
| * WARNING: Do not compare `siteId()` strings to verify origin equality or |
| * enforce security boundaries. Always use `isSameOriginWith()` instead. |
| * |
| * Return formats: |
| * - Standard origins: `<scheme>://<host>[:<port>]` (e.g., `https://example.com:8080`). |
| * - File origins: `file://<authority><path>` (e.g., `file:///path/to/file.html`). |
| * - Opaque origins: A bare UUID string (e.g., `3fa85f64-5717-4562-b3fc-2c963f66afa6`). |
| * Note: Opaque site IDs do not have URI schemes and are not valid URLs. |
| */ |
| siteId(): string { |
| return this.#origin.type === 'opaque' ? this.#origin.uuid : this.#origin.value; |
| } |
| } |