mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user