e766cc954210

fix: enforce SYNC_MANIFEST entry and tombstone count limits with regression coverage (v5.0.80)

time
2026-09-03 23:09 UTC · 1 month ago
machine
koh2
agent
claude-sonnet-4-6
session
PB-26-312 sync manifest limits
parent
d12b636cf3a3
6 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/sync-coordinator.test.ts+49 −0src/background/sync-coordinator.ts+3 −1src/shared/sync-limits.ts+12 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.79",4+  "version": "5.0.80",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "5.0.79",5+  "version": "5.0.80",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.0.79",3+  "version": "5.0.80",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/sync-coordinator.test.ts+49 −0
@@ -206,6 +206,55 @@206206   });207207 });208208 209+describe("runSyncInitiator — SYNC_MANIFEST limits (PB-26-312)", () => {210+  it("rejects oversized entry and tombstone manifests before diffing", async () => {211+    const [session, peer] = inMemoryPair();212+    const vault = makeVaultAdapter({ vaultId: "vault-abc" });213+    const initPromise = runSyncInitiator(session, vault);214+    void (async () => {215+      await peer.receive();216+      await peer.send({217+        type: "SYNC_HELLO",218+        vaultId: "vault-abc",219+        deviceId: "p",220+        version: 0,221+        deviceTime: Date.now(),222+      });223+      await peer.receive();224+      const entries = Object.fromEntries(225+        Array.from({ length: SYNC_LIMITS.MAX_ENTRY_COUNT + 1 }, (_, i) => [`id-${i}`, 1]),226+      );227+      await peer.send({ type: "SYNC_MANIFEST", entries, tombstones: {} });228+    })();229+    await expect(initPromise).resolves.toMatchObject({230+      status: "errored",231+      error: expect.stringMatching(/manifest entry count exceeds limit/),232+    });233+234+    const [tombstoneSession, tombstonePeer] = inMemoryPair();235+    const tombstonePromise = runSyncInitiator(tombstoneSession, vault);236+    void (async () => {237+      await tombstonePeer.receive();238+      await tombstonePeer.send({239+        type: "SYNC_HELLO",240+        vaultId: "vault-abc",241+        deviceId: "p",242+        version: 0,243+        deviceTime: Date.now(),244+      });245+      await tombstonePeer.receive();246+      const tombstones = Object.fromEntries(247+        Array.from({ length: SYNC_LIMITS.MAX_TOMBSTONE_COUNT + 1 }, (_, i) => [`id-${i}`, 1]),248+      );249+      await tombstonePeer.send({ type: "SYNC_MANIFEST", entries: {}, tombstones });250+    })();251+    await expect(tombstonePromise).resolves.toMatchObject({252+      status: "errored",253+      error: expect.stringMatching(/manifest tombstone count exceeds limit/),254+    });255+  });256+});257+209258 describe("runSyncInitiator — HELLO fields and version optimisation", () => {210259   it("sends HELLO with version and deviceTime", async () => {211260     const [session, peer] = inMemoryPair();

view file

src/background/sync-coordinator.ts+3 −1
@@ -25,7 +25,7 @@2525   type SignedDeviceManifest,2626 } from "../shared/sync-types";2727 import type { SyncSession } from "../shared/sync-transport";28-import { SYNC_LIMITS } from "../shared/sync-limits";28+import { SYNC_LIMITS, validateSyncManifestCounts } from "../shared/sync-limits";2929 import { isConnectionClosedError } from "../shared/sync-outcome";3030 3131 const SESSION_TIMEOUT_MS = 30_000;@@ -205,6 +205,7 @@205205     if (manifestIn.type !== "SYNC_MANIFEST") {206206       return errorOutcome(result, `expected SYNC_MANIFEST, got ${manifestIn.type}`);207207     }208+    validateSyncManifestCounts(manifestIn.entries, manifestIn.tombstones);208209 209210     const wantFromPeer = diffNewer(manifestIn.entries, ourEntries);210211     await sendMessage(session, { type: "SYNC_REQUEST", entryIds: wantFromPeer });@@ -398,6 +399,7 @@398399     if (manifestIn.type !== "SYNC_MANIFEST") {399400       return errorOutcome(result, `expected SYNC_MANIFEST, got ${manifestIn.type}`);400401     }402+    validateSyncManifestCounts(manifestIn.entries, manifestIn.tombstones);401403     await sendMessage(session, {402404       type: "SYNC_MANIFEST",403405       entries: ourEntries,

view file

src/shared/sync-limits.ts+12 −0
@@ -84,3 +84,15 @@8484   /** Max tombstones accepted in one SYNC_TOMBSTONES payload. */8585   MAX_TOMBSTONE_COUNT: 100_000,8686 } as const;87+88+export function validateSyncManifestCounts(89+  entries: Record<string, number>,90+  tombstones: Record<string, number>,91+): void {92+  if (Object.keys(entries).length > SYNC_LIMITS.MAX_ENTRY_COUNT) {93+    throw new Error("sync manifest entry count exceeds limit");94+  }95+  if (Object.keys(tombstones).length > SYNC_LIMITS.MAX_TOMBSTONE_COUNT) {96+    throw new Error("sync manifest tombstone count exceeds limit");97+  }98+}

view file