fix(browser): parse complete COOP and COEP structured items

Validate token items and their parameters before projecting exact declared policy tokens. Keep malformed and non-token values unknown; retain separate grammar for other policy headers.

Validation: real pinned Chrome public CLI baseline/candidate, 101 focused checks and 90 overlapping final checks, 834 upstream item-vector adaptations with two permitted refusals, check:fast and docs:check. Other browser/platform and policy-specific MCP delivery unverified; no performance measurement. Refs #4677.
This commit is contained in:
morluto
2026-10-17 13:26:13 +08:00
parent df2fc251e3
commit c360cfcd6d
5 changed files with 130 additions and 4 deletions
+4
View File
@@ -288,6 +288,10 @@ REA establishes a main-frame origin boundary before enabling Runtime, Debugger,
The Link projection parses repeated CDP header field lines independently and
retains their declarations in order, including derived untrusted agent hints.
Commas inside URI targets or quoted parameters remain part of those values.
COOP and COEP projections retain the exact declared token after validating the
complete Structured Field Item, including parameters such as `report-to`.
These are declarations, not an assessment of the document's effective policy;
absent, malformed or non-token items project as `null`.
REA retains local observation data and removes only URL userinfo credentials, explicitly declared scenario secrets, and values from structured credential/storage fields:
+65
View File
@@ -4,6 +4,71 @@ import { safeResponseMetadata } from "./CdpSafeMetadata.js";
const origin = "https://app.example.test";
describe("structured COOP and COEP declarations", () => {
it.each([
["same-origin", "same-origin"],
['same-origin; report-to="coop"', "same-origin"],
['require-corp; report-to="coep"', "require-corp"],
['credentialless; report-to="a; b, c"', "credentialless"],
[String.raw`same-origin; report-to="a\"b\\c"`, "same-origin"],
[
"same-origin; enabled; n=-999999999999999; f=999999999999.999",
"same-origin",
],
["same-origin; flag=?0; bin=:YQ:; date=@123", "same-origin"],
['same-origin; label=%"%c3%a9"', "same-origin"],
["same-origin; value=one; value=two", "same-origin"],
[' same-origin; report-to="coop" ', "same-origin"],
["FuturePolicy", "FuturePolicy"],
['same-origin; report-to="unterminated', null],
[String.raw`same-origin; report-to="bad\q"`, null],
["same-origin; Report-To=endpoint", null],
["same-origin ; report-to=endpoint", null],
["same-origin; report-to =endpoint", null],
["same-origin; report-to= endpoint", null],
["same-origin; report-to=", null],
["same-origin;", null],
["same-origin, require-corp", null],
["same-origin\nrequire-corp", null],
['"same-origin"; report-to="coop"', null],
["same-origin; n=1000000000000000", null],
["same-origin; f=1.1234", null],
["same-origin; b=?2", null],
["same-origin; bin=:a:", null],
["same-origin; bin=:YQ=:", null],
['same-origin; label=%"%ff"', null],
['same-origin; label=%"%C3%A9"', null],
['same-origin; label="é"', null],
["same-origin\t", null],
["", null],
[undefined, null],
] as const)(
"parses the complete structured token item %j",
(value, expected) => {
const captured = safeResponseMetadata(
"policies",
`${origin}/page`,
{
headers: {
"Cross-Origin-Opener-Policy": value,
"Cross-Origin-Embedder-Policy": value,
"Cross-Origin-Resource-Policy":
'same-origin; report-to="not-structured"',
"X-Content-Type-Options": 'nosniff; report-to="not-structured"',
},
},
new Set([origin]),
);
expect(captured.response.policies).toMatchObject({
coop: expected,
coep: expected,
corp: null,
x_content_type_options: null,
});
},
);
});
describe("safe CDP response metadata", () => {
it("drops credential headers and preserves complete local link URLs", () => {
const captured = safeResponseMetadata(
+3 -2
View File
@@ -1,6 +1,7 @@
import type { WebPageInspection } from "../domain/browserObservationSchemas.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import { cdpStringValue, type UnknownRecord } from "./CdpCaptureValues.js";
import { structuredFieldToken } from "./StructuredFieldToken.js";
type ResponseMetadata = WebPageInspection["metadata"]["responses"][number];
type LinkMetadata = ResponseMetadata["links"][number];
@@ -44,8 +45,8 @@ export const safeResponseMetadata = (
csp,
links,
policies: {
coop: policyToken(headers.get("cross-origin-opener-policy")),
coep: policyToken(headers.get("cross-origin-embedder-policy")),
coop: structuredFieldToken(headers.get("cross-origin-opener-policy")),
coep: structuredFieldToken(headers.get("cross-origin-embedder-policy")),
corp: policyToken(headers.get("cross-origin-resource-policy")),
referrer_policy: referrerPolicy(headers.get("referrer-policy")),
x_content_type_options: policyToken(
+47
View File
@@ -0,0 +1,47 @@
// RFC 9651 sections 4.2.3–4.2.10. Consume complete items, without retaining
// parameter values that the response-declaration projection does not expose.
const token = /[A-Za-z*][A-Za-z0-9!#$%&'*+.^_`|~:/\x2d]*/uy;
const key = /[a-z*][a-z0-9_.*\x2d]*/uy;
const bareItem =
/(?:[A-Za-z*][A-Za-z0-9!#$%&'*+.^_`|~:/\x2d]*|"(?:[\x20-\x21\x23-\x5b\x5d-\x7e]|\\["\\])*"|-?[0-9]{1,12}\.[0-9]{1,3}|-?[0-9]{1,15}|\?[01]|@-?[0-9]{1,15}|:(?<bytes>[A-Za-z0-9+/]*={0,2}):|%"(?<display>(?:[\x20-\x21\x23-\x24\x26-\x7e]|%[0-9a-f]{2})*)")/uy;
/** Return the exact declared token only if the entire Structured Field Item is valid. */
export const structuredFieldToken = (
value: string | undefined,
): string | null => {
if (value === undefined) return null;
let cursor = 0;
while (value[cursor] === " ") cursor += 1;
token.lastIndex = cursor;
const declared = token.exec(value)?.[0];
if (declared === undefined) return null;
cursor = token.lastIndex;
while (value[cursor] === ";") {
cursor += 1;
while (value[cursor] === " ") cursor += 1;
key.lastIndex = cursor;
if (key.exec(value) === null) return null;
cursor = key.lastIndex;
if (value[cursor] !== "=") continue;
bareItem.lastIndex = cursor + 1;
const parameter = bareItem.exec(value);
if (parameter === null) return null;
cursor = bareItem.lastIndex;
const bytes = parameter.groups?.bytes;
if (bytes !== undefined) {
const padded = bytes.endsWith("=");
if (padded ? bytes.length % 4 !== 0 : bytes.length % 4 === 1) return null;
}
const display = parameter.groups?.display;
if (display !== undefined) {
try {
// Percent syntax was checked above; this rejects invalid UTF-8 octets.
decodeURIComponent(display);
} catch {
return null;
}
}
}
while (value[cursor] === " ") cursor += 1;
return cursor === value.length ? declared : null;
};
@@ -5,7 +5,7 @@ import { inspectWebPageInputSchema } from "../../../src/domain/browserObservatio
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { trackBrowser } from "./cdpBrowserProvider.support.js";
it("preserves repeated Document Link fields and their derived agent hints", async () => {
it("preserves Document Link fields, structured policies and derived agent hints", async () => {
const browser = await startFakeCdpBrowser({
commandEvents(command, origin) {
if (command.method !== "Network.enable") return undefined;
@@ -32,7 +32,12 @@ it("preserves repeated Document Link fields and their derived agent hints", asyn
url: `${origin}/frame`,
status: 200,
mimeType: "text/html",
headers: { Link: "</next>; rel=next\n</agent>; rel=mcp" },
headers: {
Link: "</next>; rel=next\n</agent>; rel=mcp",
"Cross-Origin-Opener-Policy": 'same-origin; report-to="coop"',
"Cross-Origin-Embedder-Policy":
'require-corp; report-to="coep"',
},
},
},
},
@@ -52,6 +57,10 @@ it("preserves repeated Document Link fields and their derived agent hints", asyn
expect(result.value.metadata.responses).toContainEqual(
expect.objectContaining({
request_id: "repeated-document",
policies: expect.objectContaining({
coop: "same-origin",
coep: "require-corp",
}),
links: [
expect.objectContaining({
href: `${browser.allowedOrigin}/next`,