mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 06:46:10 +00:00
* fix(browser): classify CSP host sources by their own origin A Content-Security-Policy host-source without a scheme (for example `cdn.example.com` or `*.trusted.test`) was resolved with `new URL(token, baseUrl)`, so it became a page-relative path and was reported as the observed page's own origin. Third-party script, style, and connect sources were therefore misreported as approved origins, and a bare `*` wildcard collapsed to the page origin as well. Resolve host-sources per CSP3 \u00a72.3.1: a scheme-less source inherits the protected resource's scheme, wildcard host/port sources stay classified as a set of origins, and non-host tokens remain `other`. Keyword and scheme sources are unchanged. Adds boundary coverage for scheme-less host sources, wildcards, and scheme inheritance against the approved-origin set. * fix(browser): validate CSP host source syntax --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
193 lines
5.8 KiB
TypeScript
193 lines
5.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { safeResponseMetadata } from "../../../src/browser/CdpSafeMetadata.js";
|
|
|
|
const origin = "https://app.example.test";
|
|
|
|
describe("safe CDP response metadata", () => {
|
|
it("drops credential headers and preserves complete local link URLs", () => {
|
|
const captured = safeResponseMetadata(
|
|
"request-1",
|
|
`${origin}/api`,
|
|
{
|
|
mimeType: "application/json",
|
|
headers: {
|
|
Authorization: "Bearer credential-secret",
|
|
Cookie: "session=cookie-secret",
|
|
"Set-Cookie": "session=response-secret",
|
|
Link: `</agent?token=link-secret>; rel="mcp service-desc"; title="a,b", <https://private.example.test/x>; rel="mcp"`,
|
|
"Content-Security-Policy":
|
|
"default-src 'self'; script-src 'nonce-nonce-secret' 'sha256-hash-secret' https://private.example.test",
|
|
"Permissions-Policy":
|
|
"camera=(), geolocation=(self), invalid secret=()",
|
|
"X-Model-Context": "agent-header-secret",
|
|
},
|
|
},
|
|
new Set([origin]),
|
|
);
|
|
|
|
expect(captured.response).toMatchObject({
|
|
url: `${origin}/api`,
|
|
csp: {
|
|
nonce_count: 1,
|
|
hash_count: 1,
|
|
directives: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: "script-src",
|
|
sources: expect.arrayContaining([
|
|
{ kind: "external_origin", value: null },
|
|
]),
|
|
}),
|
|
]),
|
|
},
|
|
links: [
|
|
{
|
|
href: `${origin}/agent?token=link-secret`,
|
|
destination_scope: "approved",
|
|
rel: ["mcp", "service-desc"],
|
|
as: null,
|
|
type: null,
|
|
crossorigin: null,
|
|
},
|
|
expect.objectContaining({
|
|
href: "https://private.example.test/x",
|
|
destination_scope: "outside_policy",
|
|
}),
|
|
],
|
|
policies: { permissions_policy_features: ["camera", "geolocation"] },
|
|
});
|
|
expect(captured.agentHints).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ mechanism: "link_rel" }),
|
|
expect.objectContaining({
|
|
mechanism: "response_header",
|
|
declaration: "x-model-context",
|
|
}),
|
|
]),
|
|
);
|
|
const serialized = JSON.stringify(captured);
|
|
for (const secret of [
|
|
"credential-secret",
|
|
"cookie-secret",
|
|
"response-secret",
|
|
"nonce-secret",
|
|
"hash-secret",
|
|
"agent-header-secret",
|
|
])
|
|
expect(serialized).not.toContain(secret);
|
|
expect(serialized).toContain("link-secret");
|
|
expect(serialized).toContain("private.example.test/x");
|
|
});
|
|
|
|
it("reports well-known agent resources as untrusted observations", () => {
|
|
const captured = safeResponseMetadata(
|
|
"request-2",
|
|
`${origin}/.well-known/mcp?token=secret`,
|
|
{ headers: {} },
|
|
new Set([origin]),
|
|
);
|
|
|
|
expect(captured.agentHints).toEqual([
|
|
{
|
|
mechanism: "well_known_resource",
|
|
declaration: "/.well-known/mcp",
|
|
url: `${origin}/.well-known/mcp?token=secret`,
|
|
trust: "page-declared-untrusted",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("retains every parsed header, CSP directive, link, relation, and policy feature", () => {
|
|
const csp = Array.from(
|
|
{ length: 105 },
|
|
(_, index) => `x-${String(index)} 'self'`,
|
|
).join("; ");
|
|
const links = Array.from(
|
|
{ length: 105 },
|
|
(_, index) => `</${String(index)}>; rel="mcp service-desc"`,
|
|
).join(", ");
|
|
const permissions = Array.from(
|
|
{ length: 205 },
|
|
(_, index) => `feature-${String(index)}=()`,
|
|
).join(", ");
|
|
const headers = Object.fromEntries(
|
|
Array.from({ length: 501 }, (_, index) => [
|
|
`x-extra-${String(index)}`,
|
|
"ok",
|
|
]),
|
|
);
|
|
const captured = safeResponseMetadata(
|
|
"request-1",
|
|
`${origin}/api`,
|
|
{
|
|
mimeType: "application/json",
|
|
headers: {
|
|
...headers,
|
|
"Content-Security-Policy": csp,
|
|
Link: links,
|
|
"Permissions-Policy": permissions,
|
|
"X-Model-Context": "present-after-many-headers",
|
|
},
|
|
},
|
|
new Set([origin]),
|
|
);
|
|
|
|
expect(captured.response.csp.directives).toHaveLength(105);
|
|
expect(captured.response.links).toHaveLength(105);
|
|
expect(captured.response.policies.permissions_policy_features).toHaveLength(
|
|
205,
|
|
);
|
|
expect(captured.agentHints).toContainEqual(
|
|
expect.objectContaining({ declaration: "x-model-context" }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("safe CDP response metadata CSP sources", () => {
|
|
it("classifies scheme-less CSP host sources by their own host, not the page origin", () => {
|
|
const captured = safeResponseMetadata(
|
|
"request-csp",
|
|
`${origin}/api`,
|
|
{
|
|
headers: {
|
|
"Content-Security-Policy":
|
|
"script-src cdn.example.test *.trusted.test * 'self' https://api.other.test",
|
|
},
|
|
},
|
|
new Set([origin]),
|
|
);
|
|
|
|
expect(captured.response.csp.directives).toEqual([
|
|
{
|
|
name: "script-src",
|
|
sources: [
|
|
{ kind: "external_origin", value: null },
|
|
{ kind: "external_origin", value: null },
|
|
{ kind: "external_origin", value: null },
|
|
{ kind: "keyword", value: "'self'" },
|
|
{ kind: "external_origin", value: null },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("inherits the protected resource scheme for a scheme-less host source", () => {
|
|
const captured = safeResponseMetadata(
|
|
"request-csp-scheme",
|
|
`${origin}/api`,
|
|
{
|
|
headers: {
|
|
"Content-Security-Policy":
|
|
"script-src cdn.example.test app.example.test",
|
|
},
|
|
},
|
|
new Set([origin, "https://cdn.example.test"]),
|
|
);
|
|
|
|
expect(captured.response.csp.directives[0]?.sources).toEqual([
|
|
{ kind: "approved_origin", value: "https://cdn.example.test" },
|
|
{ kind: "approved_origin", value: origin },
|
|
]);
|
|
});
|
|
});
|