3d3cfda6a120

fix: PCH-01-012 require Number.isInteger on i/t/s/S in parseRawScan and parseChunkEnvelope — reject NaN/Infinity/fractional chunk indices at parse time (v3.8.1)

time
2026-06-10 19:53 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
pch-01 fix integration
parent
a384d9d6d53d
4 files src/popup/codex/codex.adversarial.test.ts+10 −0src/popup/codex/pdf-import.test.ts+8 −0src/popup/codex/pdf-import.ts+3 −3src/popup/codex/restore.ts+3 −3
src/popup/codex/codex.adversarial.test.ts+10 −0
@@ -264,6 +264,16 @@264264     const res = await buf.add(env);265265     expect(res.accepted).toBe(true);266266   });267+268+  // PCH-01-012: fractional i/t/s/S must be rejected by parseRawScan (NaN/Infinity269+  // are not JSON-representable, so fractional is the reachable hostile case).270+  it("parse_raw_scan_rejects_non_integer_index", async () => {271+    const env = await makeEnv({ index: 0, total: 5 });272+    expect(parseRawScan(JSON.stringify({ ...env, i: 1.5 }))).toEqual({ error: "parse_error" });273+    expect(parseRawScan(JSON.stringify({ ...env, t: 5.5 }))).toEqual({ error: "parse_error" });274+    expect(parseRawScan(JSON.stringify({ ...env, s: 0.5 }))).toEqual({ error: "parse_error" });275+    expect(parseRawScan(JSON.stringify({ ...env, S: 1.5 }))).toEqual({ error: "parse_error" });276+  });267277 });268278 269279 // ── verifyChunkIntegrity adversarial ────────────────────────────────────────

view file

src/popup/codex/pdf-import.test.ts+8 −0
@@ -86,6 +86,14 @@8686     expect(parseChunkEnvelope(JSON.stringify(broken))).toBeNull();8787   });8888 89+  // PCH-01-012: non-integer indices/stripe fields must be rejected at parse time.90+  it("parse_chunk_envelope_rejects_non_integer_stripe_fields", () => {91+    expect(parseChunkEnvelope(JSON.stringify({ ...v3, s: 0.5 }))).toBeNull();92+    expect(parseChunkEnvelope(JSON.stringify({ ...v3, S: 1.5 }))).toBeNull();93+    expect(parseChunkEnvelope(JSON.stringify({ ...v3, i: 1.5 }))).toBeNull();94+    expect(parseChunkEnvelope(JSON.stringify({ ...v3, t: 15.5 }))).toBeNull();95+  });96+8997   it("returns null for non-JSON input", () => {9098     expect(parseChunkEnvelope("not json at all")).toBeNull();9199   });

view file

src/popup/codex/pdf-import.ts+3 −3
@@ -646,15 +646,15 @@646646 647647   const baseShapeOk =648648     typeof obj.c === "string" &&649-    typeof obj.i === "number" &&650-    typeof obj.t === "number" &&649+    Number.isInteger(obj.i) && // PCH-01-012: reject NaN/Infinity/fractional indices650+    Number.isInteger(obj.t) &&651651     typeof obj.p === "boolean" &&652652     typeof obj.d === "string" &&653653     typeof obj.h === "string";654654   if (!baseShapeOk) return null;655655 656656   // v3 envelopes must also carry stripe metadata.657-  if (obj.v === 3 && (typeof obj.s !== "number" || typeof obj.S !== "number")) {657+  if (obj.v === 3 && (!Number.isInteger(obj.s) || !Number.isInteger(obj.S))) {658658     return null;659659   }660660 

view file

src/popup/codex/restore.ts+3 −3
@@ -284,8 +284,8 @@284284 285285   const looksLikeEnvelope =286286     typeof obj.c === "string" &&287-    typeof obj.i === "number" &&288-    typeof obj.t === "number" &&287+    Number.isInteger(obj.i) && // PCH-01-012: reject NaN/Infinity/fractional indices288+    Number.isInteger(obj.t) &&289289     typeof obj.p === "boolean" &&290290     typeof obj.d === "string" &&291291     typeof obj.h === "string";@@ -296,7 +296,7 @@296296 297297   // v3-only fields must be shaped correctly when present.298298   if (obj.v === 3) {299-    if (typeof obj.s !== "number" || typeof obj.S !== "number") {299+    if (!Number.isInteger(obj.s) || !Number.isInteger(obj.S)) {300300       return { error: "parse_error" };301301     }302302   }

view file