Skip to content

[high] Peer-sync Transfer overwrites un-indexed receiver files with no history; reserved-name gap #106

Description

@mbertschler

Summary

Two related receiver-side gaps in peer sync let an initiator overwrite or clobber receiver bytes with no history — breaking the append-only guarantee on the receiving node.

5a — Transfer disposition overwrites an un-indexed receiver file with no history (HIGH)

Where: agent/sync.go (planSession pre-stages copy-from-existing / supersede / conflict, but not Transfer; classifyMissingPath does no disk stat); sync/node.go (invokeRclone builds copy with no --backup-dir for node syncs).

Scenario: A receiver gains a file at path P out-of-band (web app / scp / created since last index), never indexed, so no present row. An initiator holds different content at P and pushes. classifyGetByPath(P) NotFound → Transfer. Transfer is in scope; the initiator's rclone overwrites P and, because the receiver only pre-moves the other three dispositions to .squirrel-history, the out-of-band bytes are destroyed with no preservation.

The prior SAFETY-AUDIT (§C2) waved this off: "--checksum --hash blake3 would catch the divergence as a transfer rather than a no-op." That conflates detecting the divergence with preserving it — deciding to transfer overwrites, and without --backup-dir the prior bytes are gone. The CopyFromExisting branch got the Lstat-and-move-to-history guard; Transfer did not.

Fix: give Transfer the same pre-stage — before the rclone pass, Lstat each Transfer destination; if a file exists with no live index row, move it into .squirrel-history/run-<receiverRunID>/ first (or reclassify to conflict).

5b — Receiver path validation omits two reserved names (MEDIUM)

Where: agent/sync.govalidateRelPath/validateFolderPath reject only .squirrel-history and .squirrel-conflicts; the initiator-side filter (sync/node.go isReservedSyncPath/isReservedFolderPath) correctly covers all four including .squirrel-restore-history and .squirrel-index.

Scenario: A malicious peer sends path = ".squirrel-restore-history/run-1/<existing>". The receiver accepts it (validation gap), classifies Transfer (filtered from indexing, so no row), and the initiator's rclone delivers bytes there. Combined with 5a, this overwrites the receiver's only pre-restore backup with no history.

Fix: add RestoreHistoryDirName and IndexDirName to the reserved-prefix checks in both receiver validators so the allow-list matches the initiator's filter.

Adversarial audit of offload-v1 (auditors A F1/F2, C H-1).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdata-lossCould cause silent data losssecuritySecurity / data-integrity finding

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions