[PR #707] [MERGED] fix(browser): classify CSP host sources by their own origin #2187

Closed
opened 2026-10-16 21:15:25 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/707
Author: @DeryFerd
Created: 10/6/2026
Status: ✅ Merged
Merged: 10/6/2026
Merged by: @morluto

Base: main ← Head: fix/browser-csp-host-source


📝 Commits (2)

  • a8f3a23 fix(browser): classify CSP host sources by their own origin
  • d50ba73 fix(browser): validate CSP host source syntax

📊 Changes

2 files changed (+100 additions, -6 deletions)

View changed files

📝 src/browser/CdpSafeMetadata.ts (+52 -6)
📝 tests/boundary/browser/cdpSafeMetadata.test.ts (+48 -0)

📄 Description

Summary

Content-Security-Policy sources that name a host without a scheme — cdn.example.com, *.trusted.test, or a bare * — were resolved as page-relative URLs, so the capture reported the observed page's own origin as an approved script/style/connect source instead of the third-party host the header actually names. This PR resolves each host-source to the origin it denotes, so inspect_web_page metadata reflects what the policy says rather than folding external hosts into the page origin.

Problem and expected behavior

safeResponseMetadata projects the allowlisted CSP fields returned by inspect_web_page. Its cspSource helper classified a token with new URL(token, baseUrl), where baseUrl is the response URL. For a scheme-less host-source this treats the token as a relative path, so cdn.example.com becomes https://<page>/cdn.example.com and its origin resolves to the page itself. The same happens for *.trusted.test and for a bare *.

The smallest trigger is a single response header, with the observed page served from https://app.example.test:

Content-Security-Policy: script-src cdn.example.com *.trusted.test * 'self' https://api.other.test

Before this change the two host sources and the wildcard all resolved to { kind: "approved_origin", value: "https://app.example.test" }. The invariant being violated is that a CSP host-source denotes its own host, not the origin of the protected resource. Per CSP Level 3 §2.3.1 a host-source is matched against the host it names, and a scheme-less host-source inherits the protected resource's scheme rather than being resolved as a path relative to it.

The expected projection is that cdn.example.com and *.trusted.test are reported as origins other than the page, * is reported as a set of origins, and a scheme-less host that does match an approved origin is still recognized as that approved origin.

Change and scope

cspSource now routes non-keyword, non-scheme tokens through a cspHostSourceOrigin helper that follows the host-source grammar instead of page-relative resolution. It splits an optional scheme, an authority, and an optional path; when the token omits the scheme it inherits the scheme of the protected resource URL; and it returns the origin only when the authority is a bare host with an optional port. Wildcard hosts and ports are recognized as a set of origins rather than a single origin, so *.trusted.test and * stay classified as external, and tokens that are not host-sources at all keep the existing other classification. Keyword sources such as 'self' and 'none', scheme sources such as data: and https:, and fully qualified origins like https://cdn.example.com are unchanged.

The scope is deliberately limited to the classification of one token. The zod enum that defines the allowed kind values is not touched, the surrounding directive parser and nonce/hash counting are not touched, and no other metadata projection changes. A separate, unrelated question — whether a wildcard should carry its matched set of origins in value rather than null — is left as-is because the existing schema uses null for "not a single origin" everywhere else.

Contract and boundary impact

  • Semantic owner and earliest changed stage: src/browser/CdpSafeMetadata.ts, the cspSource helper reached from safeResponseMetadata during browser response-metadata projection.
  • CLI and MCP/tool-catalog contract: none. browserCspSourceSchema and every tool input/output schema are unchanged; no generated catalog regeneration is required.
  • Provider, bridge, target-format, or platform compatibility: none. The change reads no new data from CDP and applies on every platform.
  • Evidence, artifact, provenance, or reconstruction contract: none. The field stays a typed projection with the same observed-versus-derived meaning.
  • Process execution, authorization, cleanup, or containment impact: none.
  • Generated metadata (docs/product-catalog.json), package, or installation impact: none.

Evidence and regression coverage

  • Tests added or updated: tests/boundary/browser/cdpSafeMetadata.test.ts gains a safe CDP response metadata CSP sources block with two cases — one asserting that scheme-less host sources, a wildcard host, and a bare * are not folded into the page origin, and one asserting that a scheme-less host source inherits the protected resource scheme and is still matched against the approved-origin set.
  • Base reproduction or other evidence: the header shown above reproduced the misclassification directly against the built safeResponseMetadata before the change and produces the corrected projection after it. I also checked keyword, scheme, wildcard, explicit-port, path-bearing, and fully qualified sources by hand; all classify as expected.
  • User-visible CLI/MCP output (if applicable): the metadata.responses[].csp.directives[].sources array in inspect_web_page output is the affected field. A source that previously read { "kind": "approved_origin", "value": "https://app.example.test" } now reads { "kind": "external_origin", "value": null } for an external host.
  • Remaining proof gaps: the change is a pure projection verified by unit tests, so no real browser run was needed. The real-browser CI lane is unaffected because it exercises capture, not this classification.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npm run build:cached — success.
  • npm run test:focused -- tests/boundary/browser/cdpSafeMetadata.test.ts — 5 passed.
  • npm run check:fast — typecheck and lint pass.
  • npx tsc --noEmit — pass.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. The kind and value shapes are unchanged; only the classification of previously misparsed tokens changes.
  • Real Hopper/Ghidra, browser, or OS coverage: not applicable. This is provider-neutral metadata normalization and needs no provider or OS run.
  • Package or release metadata impact: none. No dependency, package, or generated metadata change.
  • Security, privacy, process, or containment review: this is a fidelity fix in a security-relevant projection. Reporting an external CSP host as the page's own origin could lead an analyst or agent to conclude a page trusts only itself, so correcting it makes the reported policy match the header. It reads no new input and widens no authority.

Review checklist

  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above. (No issue exists for this; I searched open and closed issues and pull requests first.)
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed. (None needed.)
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/707 **Author:** [@DeryFerd](https://github.com/DeryFerd) **Created:** 10/6/2026 **Status:** ✅ Merged **Merged:** 10/6/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `fix/browser-csp-host-source` --- ### 📝 Commits (2) - [`a8f3a23`](https://github.com/morluto/rea/commit/a8f3a23fb6b83ffe06eefd0894e03a5d8f187d47) fix(browser): classify CSP host sources by their own origin - [`d50ba73`](https://github.com/morluto/rea/commit/d50ba739185d1c547f438726cbdb9b563d4697b2) fix(browser): validate CSP host source syntax ### 📊 Changes **2 files changed** (+100 additions, -6 deletions) <details> <summary>View changed files</summary> 📝 `src/browser/CdpSafeMetadata.ts` (+52 -6) 📝 `tests/boundary/browser/cdpSafeMetadata.test.ts` (+48 -0) </details> ### 📄 Description ## Summary Content-Security-Policy sources that name a host without a scheme — `cdn.example.com`, `*.trusted.test`, or a bare `*` — were resolved as page-relative URLs, so the capture reported the observed page's own origin as an approved script/style/connect source instead of the third-party host the header actually names. This PR resolves each host-source to the origin it denotes, so `inspect_web_page` metadata reflects what the policy says rather than folding external hosts into the page origin. ## Problem and expected behavior `safeResponseMetadata` projects the allowlisted CSP fields returned by `inspect_web_page`. Its `cspSource` helper classified a token with `new URL(token, baseUrl)`, where `baseUrl` is the response URL. For a scheme-less host-source this treats the token as a relative path, so `cdn.example.com` becomes `https://<page>/cdn.example.com` and its origin resolves to the page itself. The same happens for `*.trusted.test` and for a bare `*`. The smallest trigger is a single response header, with the observed page served from `https://app.example.test`: ``` Content-Security-Policy: script-src cdn.example.com *.trusted.test * 'self' https://api.other.test ``` Before this change the two host sources and the wildcard all resolved to `{ kind: "approved_origin", value: "https://app.example.test" }`. The invariant being violated is that a CSP host-source denotes its own host, not the origin of the protected resource. Per CSP Level 3 §2.3.1 a host-source is matched against the host it names, and a scheme-less host-source inherits the protected resource's scheme rather than being resolved as a path relative to it. The expected projection is that `cdn.example.com` and `*.trusted.test` are reported as origins other than the page, `*` is reported as a set of origins, and a scheme-less host that does match an approved origin is still recognized as that approved origin. ## Change and scope `cspSource` now routes non-keyword, non-scheme tokens through a `cspHostSourceOrigin` helper that follows the host-source grammar instead of page-relative resolution. It splits an optional scheme, an authority, and an optional path; when the token omits the scheme it inherits the scheme of the protected resource URL; and it returns the origin only when the authority is a bare host with an optional port. Wildcard hosts and ports are recognized as a set of origins rather than a single origin, so `*.trusted.test` and `*` stay classified as external, and tokens that are not host-sources at all keep the existing `other` classification. Keyword sources such as `'self'` and `'none'`, scheme sources such as `data:` and `https:`, and fully qualified origins like `https://cdn.example.com` are unchanged. The scope is deliberately limited to the classification of one token. The zod enum that defines the allowed `kind` values is not touched, the surrounding directive parser and nonce/hash counting are not touched, and no other metadata projection changes. A separate, unrelated question — whether a wildcard should carry its matched set of origins in `value` rather than `null` — is left as-is because the existing schema uses `null` for "not a single origin" everywhere else. ## Contract and boundary impact - Semantic owner and earliest changed stage: `src/browser/CdpSafeMetadata.ts`, the `cspSource` helper reached from `safeResponseMetadata` during browser response-metadata projection. - CLI and MCP/tool-catalog contract: none. `browserCspSourceSchema` and every tool input/output schema are unchanged; no generated catalog regeneration is required. - Provider, bridge, target-format, or platform compatibility: none. The change reads no new data from CDP and applies on every platform. - Evidence, artifact, provenance, or reconstruction contract: none. The field stays a typed projection with the same observed-versus-derived meaning. - Process execution, authorization, cleanup, or containment impact: none. - Generated metadata (`docs/product-catalog.json`), package, or installation impact: none. ## Evidence and regression coverage - Tests added or updated: `tests/boundary/browser/cdpSafeMetadata.test.ts` gains a `safe CDP response metadata CSP sources` block with two cases — one asserting that scheme-less host sources, a wildcard host, and a bare `*` are not folded into the page origin, and one asserting that a scheme-less host source inherits the protected resource scheme and is still matched against the approved-origin set. - Base reproduction or other evidence: the header shown above reproduced the misclassification directly against the built `safeResponseMetadata` before the change and produces the corrected projection after it. I also checked keyword, scheme, wildcard, explicit-port, path-bearing, and fully qualified sources by hand; all classify as expected. - User-visible CLI/MCP output (if applicable): the `metadata.responses[].csp.directives[].sources` array in `inspect_web_page` output is the affected field. A source that previously read `{ "kind": "approved_origin", "value": "https://app.example.test" }` now reads `{ "kind": "external_origin", "value": null }` for an external host. - Remaining proof gaps: the change is a pure projection verified by unit tests, so no real browser run was needed. The real-browser CI lane is unaffected because it exercises capture, not this classification. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `npm run build:cached` — success. - `npm run test:focused -- tests/boundary/browser/cdpSafeMetadata.test.ts` — 5 passed. - `npm run check:fast` — typecheck and lint pass. - `npx tsc --noEmit` — pass. ## Compatibility, safety, and release - Breaking changes or migration steps: none. The `kind` and `value` shapes are unchanged; only the classification of previously misparsed tokens changes. - Real Hopper/Ghidra, browser, or OS coverage: not applicable. This is provider-neutral metadata normalization and needs no provider or OS run. - Package or release metadata impact: none. No dependency, package, or generated metadata change. - Security, privacy, process, or containment review: this is a fidelity fix in a security-relevant projection. Reporting an external CSP host as the page's own origin could lead an analyst or agent to conclude a page trusts only itself, so correcting it makes the reported policy match the header. It reads no new input and widens no authority. ## Review checklist - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. (No issue exists for this; I searched open and closed issues and pull requests first.) - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. (None needed.) - [x] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-16 21:15:25 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2187
No description provided.