mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(browser): preserve uncertainty in malformed network observations
Require observed request method and initiator metadata instead of inventing defaults. Preserve valid redirect history while excluding later events for malformed reused IDs. Keep unavailable or invalid encoded lengths unknown and retain valid zero values. Validation: 57 CDP boundary tests plus temporary-directory hygiene and combined checks. Protocol events come from the fake CDP server; no live browser verification is claimed.
This commit is contained in:
@@ -226,10 +226,17 @@ export const handleRequestWillBeSent = (
|
||||
preserveMalformedRedirectEvidence(state, requestId, previous)
|
||||
)
|
||||
return;
|
||||
if (request === undefined) {
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
state.network.delete(requestId);
|
||||
state.networkRequestTimestamps.delete(requestId);
|
||||
const method = cdpStringValue(request?.method);
|
||||
const initiator = recordValue(params.initiator);
|
||||
const initiatorType = cdpStringValue(initiator?.type);
|
||||
if (
|
||||
request === undefined ||
|
||||
method === undefined ||
|
||||
method.length === 0 ||
|
||||
initiatorType === undefined ||
|
||||
initiatorType.length === 0
|
||||
) {
|
||||
preserveMalformedRedirectEvidence(state, requestId, previous);
|
||||
return;
|
||||
}
|
||||
const sanitized = allowedSanitizedUrl(request.url, state.allowedOrigins);
|
||||
@@ -273,6 +280,12 @@ export const handleRequestWillBeSent = (
|
||||
state.networkRequestTimestamps.delete(requestId);
|
||||
return;
|
||||
} else {
|
||||
const encodedDataLength = numberValue(redirectResponse.encodedDataLength);
|
||||
if (encodedDataLength === undefined || encodedDataLength < 0)
|
||||
state.completeness.exclude(
|
||||
"network_requests",
|
||||
"invalid_protocol_value",
|
||||
);
|
||||
redirects.push({
|
||||
url: previous.url,
|
||||
response_url: responseUrl.url,
|
||||
@@ -280,10 +293,10 @@ export const handleRequestWillBeSent = (
|
||||
resource_type: previous.resource_type,
|
||||
status: numberValue(redirectResponse.status) ?? null,
|
||||
mime_type: cdpStringValue(redirectResponse.mimeType) ?? null,
|
||||
encoded_data_length: (() => {
|
||||
const length = numberValue(redirectResponse.encodedDataLength);
|
||||
return length === undefined ? null : Math.max(0, length);
|
||||
})(),
|
||||
encoded_data_length:
|
||||
encodedDataLength === undefined || encodedDataLength < 0
|
||||
? null
|
||||
: encodedDataLength,
|
||||
request_timestamp:
|
||||
state.networkRequestTimestamps.get(requestId) ?? null,
|
||||
// CDP reports redirectResponse on the next requestWillBeSent event.
|
||||
@@ -292,7 +305,6 @@ export const handleRequestWillBeSent = (
|
||||
});
|
||||
}
|
||||
}
|
||||
const initiator = recordValue(params.initiator);
|
||||
const initiatorFrame = initiatorLocation(initiator);
|
||||
const rawInitiatorUrl = cdpStringValue(initiatorFrame?.url);
|
||||
const initiatorUrl = allowedSanitizedUrl(
|
||||
@@ -308,14 +320,14 @@ export const handleRequestWillBeSent = (
|
||||
request_id: requestId,
|
||||
url: sanitized.url,
|
||||
origin: sanitized.origin ?? "",
|
||||
method: cdpStringValue(request.method) ?? "GET",
|
||||
method,
|
||||
resource_type: cdpStringValue(params.type) ?? null,
|
||||
status: null,
|
||||
mime_type: null,
|
||||
encoded_data_length: null,
|
||||
redirects,
|
||||
initiator: {
|
||||
type: cdpStringValue(initiator?.type) ?? "other",
|
||||
type: initiatorType,
|
||||
url: initiatorUrl?.url ?? null,
|
||||
line: integerOrNull(initiatorFrame?.lineNumber),
|
||||
column: integerOrNull(initiatorFrame?.columnNumber),
|
||||
@@ -411,12 +423,14 @@ export const handleLoadingFinished = (
|
||||
if (state.malformedRedirectRequestIds.has(requestId)) return;
|
||||
const current = state.network.get(requestId);
|
||||
if (current === undefined) return;
|
||||
const encodedDataLength = numberValue(params.encodedDataLength);
|
||||
if (encodedDataLength === undefined || encodedDataLength < 0) {
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
return;
|
||||
}
|
||||
state.network.set(requestId, {
|
||||
...current,
|
||||
encoded_data_length: Math.max(
|
||||
0,
|
||||
numberValue(params.encodedDataLength) ?? 0,
|
||||
),
|
||||
encoded_data_length: encodedDataLength,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ it("preserves unavailable response observations and captures completed peers", a
|
||||
headers: { "Content-Type": "application/json" },
|
||||
postData: '{"selected":true}',
|
||||
},
|
||||
initiator: { type: "script" },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -169,7 +170,339 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
describe("CdpBrowserProvider: network 1", () => {
|
||||
describe("malformed CDP network request metadata", () => {
|
||||
it("omits requests with missing required metadata and reports the malformed events", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandEvents(command, origin) {
|
||||
if (command.method !== "Network.enable") return undefined;
|
||||
const session =
|
||||
command.sessionId === undefined
|
||||
? {}
|
||||
: { sessionId: command.sessionId };
|
||||
return [
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "valid",
|
||||
request: { url: `${origin}/valid`, method: "GET" },
|
||||
initiator: { type: "future-protocol-type" },
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "missing-method",
|
||||
request: { url: `${origin}/missing-method` },
|
||||
initiator: { type: "script" },
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "empty-method",
|
||||
request: { url: `${origin}/empty-method`, method: "" },
|
||||
initiator: { type: "script" },
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "missing-initiator-type",
|
||||
request: {
|
||||
url: `${origin}/missing-initiator-type`,
|
||||
method: "POST",
|
||||
},
|
||||
initiator: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "missing-request",
|
||||
request: null,
|
||||
initiator: { type: "script" },
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
trackBrowser(browser);
|
||||
const result = await new CdpBrowserProvider().inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
target_id: "allowed-page",
|
||||
observation_ms: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.network.requests).toEqual([
|
||||
expect.objectContaining({
|
||||
request_id: "valid",
|
||||
method: "GET",
|
||||
initiator: expect.objectContaining({ type: "future-protocol-type" }),
|
||||
}),
|
||||
]);
|
||||
expect(result.value.completeness.excluded).toContainEqual({
|
||||
section: "network_requests",
|
||||
reason: "invalid_protocol_value",
|
||||
count: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("malformed reused CDP request IDs", () => {
|
||||
it("keeps the last valid request and redirects after a malformed replacement", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandEvents(command, origin) {
|
||||
if (command.method !== "Network.enable") return undefined;
|
||||
const session =
|
||||
command.sessionId === undefined
|
||||
? {}
|
||||
: { sessionId: command.sessionId };
|
||||
return [
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "redirecting",
|
||||
request: { url: `${origin}/first`, method: "POST" },
|
||||
initiator: { type: "parser" },
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "redirecting",
|
||||
request: { url: `${origin}/second`, method: "GET" },
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: {
|
||||
url: `${origin}/first`,
|
||||
status: 301,
|
||||
mimeType: "text/plain",
|
||||
encodedDataLength: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.responseReceived",
|
||||
params: {
|
||||
requestId: "redirecting",
|
||||
response: {
|
||||
url: `${origin}/second`,
|
||||
status: 202,
|
||||
mimeType: "application/json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "redirecting",
|
||||
request: { url: `${origin}/third` },
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: {
|
||||
url: `${origin}/second`,
|
||||
status: 302,
|
||||
mimeType: "text/plain",
|
||||
encodedDataLength: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.responseReceived",
|
||||
params: {
|
||||
requestId: "redirecting",
|
||||
response: {
|
||||
url: `${origin}/third`,
|
||||
status: 418,
|
||||
mimeType: "application/json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.loadingFinished",
|
||||
params: { requestId: "redirecting", encodedDataLength: 777 },
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
trackBrowser(browser);
|
||||
const result = await new CdpBrowserProvider().inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
target_id: "allowed-page",
|
||||
observation_ms: 0,
|
||||
include_json_body_shapes: true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.network.requests).toEqual([
|
||||
expect.objectContaining({
|
||||
request_id: "redirecting",
|
||||
url: `${browser.allowedOrigin}/second`,
|
||||
method: "GET",
|
||||
status: 202,
|
||||
mime_type: "application/json",
|
||||
encoded_data_length: null,
|
||||
redirects: [
|
||||
expect.objectContaining({
|
||||
url: `${browser.allowedOrigin}/first`,
|
||||
response_url: `${browser.allowedOrigin}/first`,
|
||||
method: "POST",
|
||||
status: 301,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
expect(result.value.completeness.excluded).toContainEqual({
|
||||
section: "network_requests",
|
||||
reason: "invalid_protocol_value",
|
||||
count: 1,
|
||||
});
|
||||
expect(result.value.completeness.excluded).toContainEqual({
|
||||
section: "json_body_shapes",
|
||||
reason: "invalid_protocol_value",
|
||||
count: 1,
|
||||
});
|
||||
expect(
|
||||
browser.commands.filter(
|
||||
({ method }) => method === "Network.getResponseBody",
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CDP network length metadata", () => {
|
||||
it("retains valid zero lengths and excludes malformed loading lengths", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandEvents(command, origin) {
|
||||
if (command.method !== "Network.enable") return undefined;
|
||||
const session =
|
||||
command.sessionId === undefined
|
||||
? {}
|
||||
: { sessionId: command.sessionId };
|
||||
const request = (requestId: string, path: string) => ({
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId,
|
||||
request: { url: `${origin}${path}`, method: "GET" },
|
||||
initiator: { type: "parser" },
|
||||
},
|
||||
});
|
||||
return [
|
||||
request("zero", "/zero"),
|
||||
request("missing", "/missing"),
|
||||
request("negative", "/negative"),
|
||||
request("prior", "/prior"),
|
||||
request("redirect-zero", "/redirect-zero-first"),
|
||||
request("redirect-invalid", "/redirect-invalid-first"),
|
||||
{
|
||||
...session,
|
||||
method: "Network.loadingFinished",
|
||||
params: { requestId: "zero", encodedDataLength: 0 },
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.loadingFinished",
|
||||
params: { requestId: "missing" },
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.loadingFinished",
|
||||
params: { requestId: "negative", encodedDataLength: -1 },
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.loadingFinished",
|
||||
params: { requestId: "prior", encodedDataLength: 27 },
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.loadingFinished",
|
||||
params: { requestId: "prior", encodedDataLength: -1 },
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "redirect-zero",
|
||||
request: { url: `${origin}/redirect-zero-final`, method: "GET" },
|
||||
initiator: { type: "parser" },
|
||||
redirectResponse: {
|
||||
url: `${origin}/redirect-zero-first`,
|
||||
status: 301,
|
||||
encodedDataLength: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "redirect-invalid",
|
||||
request: {
|
||||
url: `${origin}/redirect-invalid-final`,
|
||||
method: "GET",
|
||||
},
|
||||
initiator: { type: "parser" },
|
||||
redirectResponse: {
|
||||
url: `${origin}/redirect-invalid-first`,
|
||||
status: 301,
|
||||
encodedDataLength: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
trackBrowser(browser);
|
||||
const result = await new CdpBrowserProvider().inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
target_id: "allowed-page",
|
||||
observation_ms: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result.ok) throw result.error;
|
||||
const requests = new Map(
|
||||
result.value.network.requests.map((item) => [item.request_id, item]),
|
||||
);
|
||||
expect(requests.get("zero")?.encoded_data_length).toBe(0);
|
||||
expect(requests.get("missing")?.encoded_data_length).toBeNull();
|
||||
expect(requests.get("negative")?.encoded_data_length).toBeNull();
|
||||
expect(requests.get("prior")?.encoded_data_length).toBe(27);
|
||||
expect(
|
||||
requests.get("redirect-zero")?.redirects?.[0]?.encoded_data_length,
|
||||
).toBe(0);
|
||||
expect(
|
||||
requests.get("redirect-invalid")?.redirects?.[0]?.encoded_data_length,
|
||||
).toBeNull();
|
||||
expect(result.value.completeness.excluded).toContainEqual({
|
||||
section: "network_requests",
|
||||
reason: "invalid_protocol_value",
|
||||
count: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("same-origin redirect chains", () => {
|
||||
it("retains same-origin redirect hops on the one final request", async () => {
|
||||
const browser = await startFakeCdpBrowser({ redirectWithinOrigin: true });
|
||||
trackBrowser(browser);
|
||||
@@ -222,7 +555,9 @@ describe("CdpBrowserProvider: network 1", () => {
|
||||
count: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirect origin policy", () => {
|
||||
it("drops network evidence when a request redirects outside the approved origin", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
redirectToDisallowedOrigin: true,
|
||||
|
||||
@@ -316,6 +316,7 @@ const captureBody = (source: BodySource, body: string): WebPageInspection => {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
postData: source === "request" ? body : "{}",
|
||||
},
|
||||
initiator: { type: "script" },
|
||||
},
|
||||
});
|
||||
const responseBody = source === "request" ? "{}" : body;
|
||||
|
||||
+7
@@ -247,6 +247,7 @@ const emitSessionTimeline = (
|
||||
frameId: "frame-main",
|
||||
loaderId: "loader-redirect",
|
||||
request: { url: redirectUrl, method: "GET" },
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: { status: 302 },
|
||||
timestamp: 13,
|
||||
});
|
||||
@@ -294,6 +295,7 @@ const emitSameOriginRedirectEvents = (
|
||||
requestId: "request-1",
|
||||
type: "Fetch",
|
||||
request: { url: intermediate, method: "GET" },
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: {
|
||||
url,
|
||||
status: 301,
|
||||
@@ -307,6 +309,7 @@ const emitSameOriginRedirectEvents = (
|
||||
requestId: "request-1",
|
||||
type: "Fetch",
|
||||
request: { url: finalUrl, method: "GET" },
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: {
|
||||
url: intermediate,
|
||||
status: 302,
|
||||
@@ -328,6 +331,7 @@ const emitReturnFromDisallowedRedirect = (
|
||||
requestId: "request-1",
|
||||
type: "Fetch",
|
||||
request: { url: finalUrl, method: "GET" },
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: {
|
||||
url: "https://private.example.test/outside?token=redirect-url-secret",
|
||||
status: 302,
|
||||
@@ -360,6 +364,7 @@ const emitMalformedRedirectPriorResponse = (
|
||||
token: "prior-body",
|
||||
}),
|
||||
},
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: {
|
||||
url: initialUrl,
|
||||
status: 307,
|
||||
@@ -403,6 +408,7 @@ const emitMalformedRedirect = (
|
||||
requestId: "request-1",
|
||||
type: "Fetch",
|
||||
request: { url: finalUrl, method: "GET" },
|
||||
initiator: { type: "other" },
|
||||
...(options.omitRedirectResponse === true ? {} : { redirectResponse }),
|
||||
timestamp: 9,
|
||||
});
|
||||
@@ -476,6 +482,7 @@ const emitNetworkEvents = (
|
||||
url: "https://private.example.test/redirected",
|
||||
method: "GET",
|
||||
},
|
||||
initiator: { type: "other" },
|
||||
redirectResponse: {
|
||||
url,
|
||||
status: 302,
|
||||
|
||||
Reference in New Issue
Block a user