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
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.
Test: restore against a db.zst.age cut at that length returns an error and a non-zero result.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.Decryptsucceeds. The first read of the age stream returnsio.ErrUnexpectedEOF(ageinternal/stream/stream.go:96-98), but the zstd decoder, which is at the start of a frame, turns that into a cleanio.EOF(klauspost zstdframedec.go:72-75).blobgen.Reader.Readtherefore returns zero bytes and no error. A cut at any other position does produce an error.Close(internal/vaultik/blob_fetch.go:46-55).db.zst.ageis not.downloadSnapshotDB(internal/vaultik/restore.go:606-616) accepts the empty result,database.Newbuilds a fresh schema on the empty file, and restore warns "No files found to restore" and exits 0 (restore.go:119-123).verify --deepis 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://andfile://writes are atomic, so an interrupted upload is not a realistic cause.Acceptable
blobgen.Reader.Readreads once more from the age reader and returnsio.ErrUnexpectedEOFunless that read returns(0, io.EOF).downloadSnapshotDBrejects a zero-length decrypted database beforedatabase.Newapplies a schema to it.Definition of done
internal/blobgen:io.ReadAllon an input cut at header plus nonce returns an error. A genuinely empty input still round-trips to empty with no error.db.zst.agecut at that length returns an error and a non-zero result.make checkgreen.The test-coverage issue #170 lists this truncation case as depending on this fix; do not commit it skipped.
Line numbers are as of
nextat6fcd8e1.model: fable-5-1