Objects fetched from the store are untrusted; several decode paths let one expand or print without limit. - blobgen.LimitReader errors past a byte cap (not io.LimitReader silent EOF). DecodeManifest reads through caps on both compressed input and decompressed output, far above any real manifest, so json.Decode cannot buffer a compressible bomb. FetchAndDecryptBlob bounds decompression to the blob recorded uncompressed_size (not the restoring host blob_size_limit). - downloadSnapshotDB streams straight from storage to its temp file with io.Copy, replacing two ReadAll calls that held the whole database twice. - FetchBlob drops the per-blob Stat round-trip, its expectedSize parameter and returned size, all of which only fed a debug log. - TTYHandler and ui.Writer escape control characters in messages, attribute keys/values, and rendered identifiers/paths before colour codes are applied, so a crafted value cannot drive the terminal. Model: opus-4-8
80 lines
2.4 KiB
Go
80 lines
2.4 KiB
Go
//nolint:testpackage // exercises the unexported decodeManifest bounds
|
|
package snapshot
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/blobgen"
|
|
)
|
|
|
|
// testSnapshotID is a stand-in snapshot ID reused across the bound cases.
|
|
const testSnapshotID = "host_home_2026-01-01T00:00:00Z"
|
|
|
|
// TestDecodeManifestRoundTrip is the baseline: with generous bounds a
|
|
// manifest the writer produced decodes back unchanged.
|
|
func TestDecodeManifestRoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
want := &Manifest{
|
|
SnapshotID: testSnapshotID,
|
|
Timestamp: "2026-01-01T00:00:00Z",
|
|
BlobCount: 2,
|
|
TotalCompressedSize: 42,
|
|
Blobs: []BlobInfo{
|
|
{Hash: "aa", CompressedSize: 21},
|
|
{Hash: "bb", CompressedSize: 21},
|
|
},
|
|
}
|
|
|
|
compressed, err := EncodeManifest(want, 3)
|
|
require.NoError(t, err)
|
|
|
|
got, err := decodeManifest(
|
|
bytes.NewReader(compressed), manifestMaxCompressed, manifestMaxDecompressed)
|
|
require.NoError(t, err)
|
|
require.Equal(t, want, got)
|
|
}
|
|
|
|
// TestDecodeManifestBoundsDecompressedOutput feeds a valid but highly
|
|
// compressible manifest — one whose timestamp is a megabyte of the same
|
|
// character — through a small decompressed bound. The compressed form is
|
|
// tiny, so only the decompressed bound stops it; decoding must fail within
|
|
// that bound rather than expanding the value in memory.
|
|
func TestDecodeManifestBoundsDecompressedOutput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
bomb := &Manifest{
|
|
SnapshotID: testSnapshotID,
|
|
Timestamp: strings.Repeat("a", 1<<20),
|
|
}
|
|
|
|
compressed, err := EncodeManifest(bomb, 3)
|
|
require.NoError(t, err)
|
|
require.Less(t, len(compressed), 4096,
|
|
"the compressible manifest must be small compressed")
|
|
|
|
_, err = decodeManifest(bytes.NewReader(compressed), 1<<20, 4096)
|
|
require.ErrorIs(t, err, blobgen.ErrOutputTooLarge)
|
|
}
|
|
|
|
// TestDecodeManifestBoundsCompressedInput checks the compressed-input
|
|
// bound fires independently: a valid manifest with a generous decompressed
|
|
// bound but a tiny compressed bound still fails.
|
|
func TestDecodeManifestBoundsCompressedInput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifest := &Manifest{
|
|
SnapshotID: testSnapshotID,
|
|
Timestamp: strings.Repeat("a", 4096),
|
|
}
|
|
|
|
compressed, err := EncodeManifest(manifest, 3)
|
|
require.NoError(t, err)
|
|
|
|
_, err = decodeManifest(bytes.NewReader(compressed), 8, manifestMaxDecompressed)
|
|
require.Error(t, err)
|
|
}
|