A stream cut right after the age header reads as valid and empty, and restore then reports success #152

Open
opened 2026-09-22 00:54:59 +02:00 by clawbot · 0 comments
Collaborator

Found by the security review #73. Severity: medium (restore reports success while restoring nothing; no forged content is accepted).

What is wrong

For an object that is exactly the age header plus the 16-byte nonce, age.Decrypt succeeds. The first read of the age stream returns io.ErrUnexpectedEOF (age internal/stream/stream.go:96-98), but the zstd decoder, which is at the start of a frame, turns that into a clean io.EOF (klauspost zstd framedec.go:72-75). blobgen.Reader.Read therefore returns zero bytes and no error. A cut at any other position does produce an error.

  • Blobs are protected: the double hash is compared on Close (internal/vaultik/blob_fetch.go:46-55).
  • db.zst.age is not. downloadSnapshotDB (internal/vaultik/restore.go:606-616) accepts the empty result, database.New builds a fresh schema on the empty file, and restore warns "No files found to restore" and exits 0 (restore.go:119-123).
  • verify --deep is not affected; it applies no schema and its first query fails.

Who can cause it: a party who can overwrite metadata/{remote-key}/db.zst.age. No key is needed, because the existing header can be reused. s3:// and file:// writes are atomic, so an interrupted upload is not a realistic cause.

Acceptable

  • When the zstd decoder reports EOF, blobgen.Reader.Read reads once more from the age reader and returns io.ErrUnexpectedEOF unless that read returns (0, io.EOF).
  • downloadSnapshotDB rejects a zero-length decrypted database before database.New applies a schema to it.

Definition of done

  1. Test in internal/blobgen: io.ReadAll on an input cut at header plus nonce returns an error. A genuinely empty input still round-trips to empty with no error.
  2. Test: restore against a db.zst.age cut at that length returns an error and a non-zero result.
  3. No existing assertion weakened; make check green.

The test-coverage issue #170 lists this truncation case as depending on this fix; do not commit it skipped.

Line numbers are as of next at 6fcd8e1.

model: fable-5-1

Found by the security review https://git.eeqj.de/sneak/vaultik/issues/73. Severity: **medium** (restore reports success while restoring nothing; no forged content is accepted). ## What is wrong For an object that is exactly the age header plus the 16-byte nonce, `age.Decrypt` succeeds. The first read of the age stream returns `io.ErrUnexpectedEOF` (age `internal/stream/stream.go:96-98`), but the zstd decoder, which is at the start of a frame, turns that into a clean `io.EOF` (klauspost zstd `framedec.go:72-75`). `blobgen.Reader.Read` therefore returns zero bytes and no error. A cut at any other position does produce an error. - Blobs are protected: the double hash is compared on `Close` (`internal/vaultik/blob_fetch.go:46-55`). - `db.zst.age` is not. `downloadSnapshotDB` (`internal/vaultik/restore.go:606-616`) accepts the empty result, `database.New` builds a fresh schema on the empty file, and restore warns "No files found to restore" and exits 0 (`restore.go:119-123`). - `verify --deep` is not affected; it applies no schema and its first query fails. Who can cause it: a party who can overwrite `metadata/{remote-key}/db.zst.age`. No key is needed, because the existing header can be reused. `s3://` and `file://` writes are atomic, so an interrupted upload is not a realistic cause. ## Acceptable - When the zstd decoder reports EOF, `blobgen.Reader.Read` reads once more from the age reader and returns `io.ErrUnexpectedEOF` unless that read returns `(0, io.EOF)`. - `downloadSnapshotDB` rejects a zero-length decrypted database before `database.New` applies a schema to it. ## Definition of done 1. Test in `internal/blobgen`: `io.ReadAll` on an input cut at header plus nonce returns an error. A genuinely empty input still round-trips to empty with no error. 2. Test: restore against a `db.zst.age` cut at that length returns an error and a non-zero result. 3. No existing assertion weakened; `make check` green. The test-coverage issue https://git.eeqj.de/sneak/vaultik/issues/170 lists this truncation case as depending on this fix; do not commit it skipped. Line numbers are as of `next` at `6fcd8e1`. model: fable-5-1
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/vaultik#152