Found by the security review #73. Severity: low (hardening; item 1 matters on a multi-user restore machine).
What is wrong
restoreRegularFile creates each file with the default mode (internal/vaultik/restore.go:871), writes all content, closes, and only then applies the stored mode (:902). Until then another local user can read a file whose stored mode is restrictive but whose directory is not; in a path-filtered restore that is every file, because ancestor directories outside the restored set are created 0755 (:764). When writeFileChunks fails, the partial file stays in place with the default mode (:880-883). A chmod failure is only a debug line (:823-826).
hashVerifyReader.Close compares the blob hash only if Read reached EOF (internal/vaultik/blob_fetch.go:42-63); a caller that closes early gets nil. The single production caller reads to EOF and checks Close, so restore is correct today, but the one function guarding against a swapped blob passes silently when skipped. downloadBlobToCache also leaves the cache file of a blob that failed its hash until the deferred cache close (restore.go:989-999).
verifyFile (restore.go:1126-1158), used by --verify, accepts trailing bytes after the last chunk.
Acceptable
Remove any existing entry at the target, then create the file 0600 with OpenFile and O_CREATE|O_EXCL (the mode argument is ignored for an existing file, and the README documents re-running over partial output). Apply the stored mode after close. A chmod failure on a regular file is a user-visible warning. A file whose restore failed is removed.
Close returns an error when called before EOF. downloadBlobToCache deletes the cache entry when Close fails.
verifyFile confirms the file is at EOF after the last chunk.
Do not compare bytes written with files.size: that is the stat taken during the scan, while the chunker reads to EOF later, so a file that changed during backup would fail to restore. If a whole-file length check is wanted, compare against the sum of chunks.size.
Whether the default restore path should also hash every chunk before writing it is an owner question on #73; not part of this issue.
Definition of done
Tests under umask 022: a restored 0600 file is never observable with a wider mode; a failed file is removed; reading part of a blob then Close returns an error; a restored file with trailing bytes fails --verify.
No existing assertion weakened; make check green.
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: **low** (hardening; item 1 matters on a multi-user restore machine).
## What is wrong
1. `restoreRegularFile` creates each file with the default mode (`internal/vaultik/restore.go:871`), writes all content, closes, and only then applies the stored mode (`:902`). Until then another local user can read a file whose stored mode is restrictive but whose directory is not; in a path-filtered restore that is every file, because ancestor directories outside the restored set are created 0755 (`:764`). When `writeFileChunks` fails, the partial file stays in place with the default mode (`:880-883`). A chmod failure is only a debug line (`:823-826`).
2. `hashVerifyReader.Close` compares the blob hash only if `Read` reached EOF (`internal/vaultik/blob_fetch.go:42-63`); a caller that closes early gets nil. The single production caller reads to EOF and checks `Close`, so restore is correct today, but the one function guarding against a swapped blob passes silently when skipped. `downloadBlobToCache` also leaves the cache file of a blob that failed its hash until the deferred cache close (`restore.go:989-999`).
3. `verifyFile` (`restore.go:1126-1158`), used by `--verify`, accepts trailing bytes after the last chunk.
## Acceptable
- Remove any existing entry at the target, then create the file 0600 with `OpenFile` and `O_CREATE|O_EXCL` (the mode argument is ignored for an existing file, and the README documents re-running over partial output). Apply the stored mode after close. A chmod failure on a regular file is a user-visible warning. A file whose restore failed is removed.
- `Close` returns an error when called before EOF. `downloadBlobToCache` deletes the cache entry when `Close` fails.
- `verifyFile` confirms the file is at EOF after the last chunk.
- Do not compare bytes written with `files.size`: that is the stat taken during the scan, while the chunker reads to EOF later, so a file that changed during backup would fail to restore. If a whole-file length check is wanted, compare against the sum of `chunks.size`.
Whether the default restore path should also hash every chunk before writing it is an owner question on https://git.eeqj.de/sneak/vaultik/issues/73; not part of this issue.
## Definition of done
1. Tests under umask 022: a restored 0600 file is never observable with a wider mode; a failed file is removed; reading part of a blob then `Close` returns an error; a restored file with trailing bytes fails `--verify`.
2. No existing assertion weakened; `make check` green.
Line numbers are as of `next` at `6fcd8e1`.
model: fable-5-1
Regular files are created with O_EXCL at 0600, then given their stored mode only after the content is written and closed; a failed write or close removes the file; a chmod failure is a user-visible warning.
The blob verifying reader now errors if closed before EOF, and the download drops the cache entry on any copy/close failure, so an unverified blob can never be read back as valid.
--verify now rejects a restored file with bytes past its last chunk (no files.size comparison added).
Tests under umask 022 cover all four definition-of-done cases. make check is green.
Model: opus-4-8
Done in https://git.eeqj.de/sneak/vaultik/pulls/182 (base `next`).
- Regular files are created with `O_EXCL` at 0600, then given their stored mode only after the content is written and closed; a failed write or close removes the file; a chmod failure is a user-visible warning.
- The blob verifying reader now errors if closed before EOF, and the download drops the cache entry on any copy/close failure, so an unverified blob can never be read back as valid.
- `--verify` now rejects a restored file with bytes past its last chunk (no `files.size` comparison added).
Tests under umask 022 cover all four definition-of-done cases. `make check` is green.
Model: opus-4-8
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: low (hardening; item 1 matters on a multi-user restore machine).
What is wrong
restoreRegularFilecreates each file with the default mode (internal/vaultik/restore.go:871), writes all content, closes, and only then applies the stored mode (:902). Until then another local user can read a file whose stored mode is restrictive but whose directory is not; in a path-filtered restore that is every file, because ancestor directories outside the restored set are created 0755 (:764). WhenwriteFileChunksfails, the partial file stays in place with the default mode (:880-883). A chmod failure is only a debug line (:823-826).hashVerifyReader.Closecompares the blob hash only ifReadreached EOF (internal/vaultik/blob_fetch.go:42-63); a caller that closes early gets nil. The single production caller reads to EOF and checksClose, so restore is correct today, but the one function guarding against a swapped blob passes silently when skipped.downloadBlobToCachealso leaves the cache file of a blob that failed its hash until the deferred cache close (restore.go:989-999).verifyFile(restore.go:1126-1158), used by--verify, accepts trailing bytes after the last chunk.Acceptable
OpenFileandO_CREATE|O_EXCL(the mode argument is ignored for an existing file, and the README documents re-running over partial output). Apply the stored mode after close. A chmod failure on a regular file is a user-visible warning. A file whose restore failed is removed.Closereturns an error when called before EOF.downloadBlobToCachedeletes the cache entry whenClosefails.verifyFileconfirms the file is at EOF after the last chunk.files.size: that is the stat taken during the scan, while the chunker reads to EOF later, so a file that changed during backup would fail to restore. If a whole-file length check is wanted, compare against the sum ofchunks.size.Whether the default restore path should also hash every chunk before writing it is an owner question on #73; not part of this issue.
Definition of done
Closereturns an error; a restored file with trailing bytes fails--verify.make checkgreen.Line numbers are as of
nextat6fcd8e1.model: fable-5-1
Done in #182 (base
next).O_EXCLat 0600, then given their stored mode only after the content is written and closed; a failed write or close removes the file; a chmod failure is a user-visible warning.--verifynow rejects a restored file with bytes past its last chunk (nofiles.sizecomparison added).Tests under umask 022 cover all four definition-of-done cases.
make checkis green.Model: opus-4-8