Validate blob hashes, offsets and lengths from the destination (closes #155)
A blob hash read back from the downloaded snapshot database or the store listing was trusted unchecked. A hostile remote could set a hash such as "aa/../../etc" and have a decrypted blob written outside the cache directory, or feed a short or negative value that panicked a command. blobDiskCache.path now refuses any key with a path separator, and ReadAt rejects a negative offset or length, bounding so a sum cannot overflow past the check. A new isBlobHash helper gates FetchBlob, shallow and deep verify, and restore: buildBlobIndexes rejects every hash from the snapshot database before any fetch. The blobs/ and metadata/ listings skip a non-conforming name, and short-hash prefixes in log and error text go through a panic-safe shortHash helper. Model: opus-4-8
This commit was merged in pull request #195.
This commit is contained in:
@@ -730,8 +730,18 @@ func (v *Vaultik) VerifySnapshotWithOptions(
|
||||
result.DatabaseMissing = true
|
||||
}
|
||||
|
||||
result.Verified, result.Missing, result.Mismatched, result.MissingSize =
|
||||
result.Verified, result.Missing, result.Mismatched, result.MissingSize, err =
|
||||
v.verifyManifestBlobs(manifest, opts)
|
||||
if err != nil {
|
||||
if opts.JSON {
|
||||
result.Status = verifyStatusFailed
|
||||
result.ErrorMessage = fmt.Sprintf("verifying manifest blobs: %v", err)
|
||||
|
||||
return v.outputVerifyJSON(result)
|
||||
}
|
||||
|
||||
return fmt.Errorf("verifying manifest blobs: %w", err)
|
||||
}
|
||||
|
||||
return v.formatVerifyResult(result, opts)
|
||||
}
|
||||
@@ -766,13 +776,20 @@ func (v *Vaultik) printVerifyHeader(snapshotID string, opts *VerifyOptions) {
|
||||
// comparison matches the deep path (see verifyBlobExistenceFromDB).
|
||||
func (v *Vaultik) verifyManifestBlobs(
|
||||
manifest *snapshot.Manifest, opts *VerifyOptions,
|
||||
) (int, int, int, int64) {
|
||||
) (int, int, int, int64, error) {
|
||||
var (
|
||||
verified, missing, mismatched int
|
||||
missingSize int64
|
||||
)
|
||||
|
||||
for _, blob := range manifest.Blobs {
|
||||
// The manifest is unauthenticated, so its blob hashes are checked
|
||||
// before being spliced into a storage path.
|
||||
if !isBlobHash(blob.Hash) {
|
||||
return 0, 0, 0, 0, fmt.Errorf("%w: %s",
|
||||
errInvalidBlobHash, shortHash(blob.Hash))
|
||||
}
|
||||
|
||||
blobPath := fmt.Sprintf("blobs/%s/%s/%s",
|
||||
blob.Hash[:2], blob.Hash[2:4], blob.Hash)
|
||||
|
||||
@@ -798,7 +815,7 @@ func (v *Vaultik) verifyManifestBlobs(
|
||||
}
|
||||
}
|
||||
|
||||
return verified, missing, mismatched, missingSize
|
||||
return verified, missing, mismatched, missingSize, nil
|
||||
}
|
||||
|
||||
// formatVerifyResult outputs the final verification results as JSON or
|
||||
@@ -1313,21 +1330,36 @@ func (v *Vaultik) listAllRemoteSnapshotKeys() ([]string, error) {
|
||||
}
|
||||
|
||||
parts := strings.Split(object.Key, "/")
|
||||
if len(parts) >= minSnapshotIDParts &&
|
||||
parts[0] == metadataDirName && parts[1] != "" {
|
||||
// Skip macOS resource fork files (._*) and other hidden files
|
||||
if strings.HasPrefix(parts[1], ".") {
|
||||
continue
|
||||
}
|
||||
if len(parts) < minSnapshotIDParts ||
|
||||
parts[0] != metadataDirName || parts[1] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(object.Key, "/") ||
|
||||
strings.Contains(object.Key, "/manifest.json.zst") {
|
||||
key := parts[1]
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
// Skip macOS resource fork files (._*) and other hidden files
|
||||
if strings.HasPrefix(parts[1], ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(object.Key, "/") &&
|
||||
!strings.Contains(object.Key, "/manifest.json.zst") {
|
||||
continue
|
||||
}
|
||||
|
||||
key := parts[1]
|
||||
|
||||
// A remote snapshot key is a SHA-256 hash: 64 lowercase hex
|
||||
// characters. The listing comes from the untrusted destination,
|
||||
// so accept a key only in that form.
|
||||
if !isBlobHash(key) {
|
||||
log.Warn("Skipping non-conforming key under metadata/",
|
||||
"key", object.Key)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user