From c22d7cd7567b52e6e6ba13f14df11c4a720c0c4e Mon Sep 17 00:00:00 2001 From: yasshmittal Date: Tue, 13 Oct 2026 19:18:55 +0530 Subject: [PATCH] fix(browser): keep malformed Content-Length declarations unknown (#2773) * fix(browser): keep malformed Content-Length declarations unknown Response metadata normalized Content-Length with Number(), truncation and clamping, so "-5", "0x10", "1e3" and "+12" became observed lengths 0, 16, 1000 and 12. RFC 9110 section 8.6 defines the field as 1*DIGIT; any other spelling declares no length. Parse only decimal digits within the safe integer range and keep every other value null. Fixes #2596 Co-Authored-By: Claude Opus 5.5 * test(browser): cover the largest accepted Content-Length Pin the safe-integer boundary beside the overflow case so a future change cannot reject the largest representable declared length unnoticed. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- src/browser/CdpSafeMetadata.test.ts | 21 +++++++++++++++++++++ src/browser/CdpSafeMetadata.ts | 17 ++++++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/browser/CdpSafeMetadata.test.ts b/src/browser/CdpSafeMetadata.test.ts index 0cff4e094..ef9c42e14 100644 --- a/src/browser/CdpSafeMetadata.test.ts +++ b/src/browser/CdpSafeMetadata.test.ts @@ -289,4 +289,25 @@ describe("safe CDP response metadata referrer policy", () => { expect(captured.response.policies.referrer_policy).toBe(expected); expect(captured.response.policies.coop).toBe("same-origin"); }); + + it.each<[string | number | undefined, number | null]>([ + ["100", 100], + [" 0 ", 0], + [100, 100], + ["-5", null], + ["+12", null], + ["0x10", null], + ["1e3", null], + ["9007199254740991", 9007199254740991], + ["9007199254740992", null], + [undefined, null], + ])("declares Content-Length %j only for decimal digits", (raw, expected) => { + const captured = safeResponseMetadata( + "request-length", + `${origin}/api`, + { headers: raw === undefined ? {} : { "Content-Length": raw } }, + new Set([origin]), + ); + expect(captured.response.content_length).toBe(expected); + }); }); diff --git a/src/browser/CdpSafeMetadata.ts b/src/browser/CdpSafeMetadata.ts index 589c00367..1347cdcca 100644 --- a/src/browser/CdpSafeMetadata.ts +++ b/src/browser/CdpSafeMetadata.ts @@ -1,10 +1,6 @@ import type { WebPageInspection } from "../domain/browserObservationSchemas.js"; import { sanitizeBrowserUrl } from "../domain/browserObservation.js"; -import { - numberValue, - cdpStringValue, - type UnknownRecord, -} from "./CdpCaptureValues.js"; +import { cdpStringValue, type UnknownRecord } from "./CdpCaptureValues.js"; type ResponseMetadata = WebPageInspection["metadata"]["responses"][number]; type LinkMetadata = ResponseMetadata["links"][number]; @@ -43,7 +39,7 @@ export const safeResponseMetadata = ( request_id: requestId, url, mime_type: boundedHeader(cdpStringValue(response.mimeType)), - content_length: nonnegativeInteger(headers.get("content-length")), + content_length: contentLength(headers.get("content-length")), content_encoding: boundedHeader(headers.get("content-encoding")), csp, links, @@ -343,9 +339,12 @@ const policyToken = (value: string | undefined): string | null => { return /^[a-z][a-z0-9_.\x2d]*$/u.test(token) ? token : null; }; -const nonnegativeInteger = (value: string | undefined): number | null => { - const number = numberValue(value === undefined ? undefined : Number(value)); - return number === undefined ? null : Math.max(0, Math.trunc(number)); +// RFC 9110 section 8.6: Content-Length = 1*DIGIT. Other spellings declare no length. +const contentLength = (value: string | undefined): number | null => { + const token = (value ?? "").trim(); + if (!/^[0-9]+$/u.test(token)) return null; + const length = Number(token); + return Number.isSafeInteger(length) ? length : null; }; const boundedHeader = (value: string | undefined): string | null =>