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
↓ peach-browser-e766cc95.tar.gzatlas at this save
files changed6 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 −0manifest.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": {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",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",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();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,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+}