fix(cli): harden restore preflight validation

This commit is contained in:
2026-08-16 01:38:48 +02:00
parent 6618a9d7c2
commit cbb18acc4d
2 changed files with 367 additions and 15 deletions
+115 -9
View File
@@ -233,6 +233,91 @@ func TestPreflightRejectsTraversalAndSymlinkInsideVolumeTar(t *testing.T) {
}
}
func TestPreflightRejectsArchivesThatExceedConfiguredProcessingLimits(t *testing.T) {
installation := preflightTestInstallation(t)
tests := []struct {
name string
spec preflightArchiveSpec
limits PreflightLimits
wantErr string
}{
{
name: "member count",
spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "one", body: []byte("one")}}},
limits: PreflightLimits{MaxMembers: 1, MaxUncompressedBytes: 1 << 20, MaxCompressionRatio: 100},
wantErr: "member limit",
},
{
name: "uncompressed bytes",
spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "large", body: []byte("123456789")}}},
limits: PreflightLimits{MaxMembers: 10, MaxUncompressedBytes: 1, MaxCompressionRatio: 100},
wantErr: "uncompressed-size limit",
},
{
name: "compression ratio",
spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{
path: "compressed", body: []byte(strings.Repeat("A", 4096)), method: zip.Deflate,
}}},
limits: PreflightLimits{MaxMembers: 10, MaxUncompressedBytes: 1 << 20, MaxCompressionRatio: 2},
wantErr: "compression-ratio limit",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
archive := filepath.Join(t.TempDir(), "limited.zip")
writePreflightArchive(t, archive, test.spec)
_, err := Preflight(context.Background(), installation, PreflightRequest{
Archive: archive, Confirm: true, Limits: test.limits,
}, permissivePreflightDependencies())
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("Preflight() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestPreflightRevalidationRefusesAnArchivePathThatWasReplaced(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "checked.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("validated")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
replacement := filepath.Join(t.TempDir(), "replacement.zip")
writePreflightArchive(t, replacement, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("unchecked")}},
})
if err := os.Rename(replacement, archive); err != nil {
t.Fatal(err)
}
if _, err := result.RevalidateArchive(); err == nil || !strings.Contains(err.Error(), "changed") {
t.Fatalf("RevalidateArchive() error = %v, want replaced path refusal", err)
}
}
func TestPreflightRejectsArchiveEntryWithModeDifferentFromManifest(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "mode-mismatch.zip")
manifestMode := uint32(0o600)
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{
path: "configuration/operator.env", body: []byte("safe"), mode: 0o644, manifestMode: &manifestMode,
}},
})
_, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err == nil || !strings.Contains(err.Error(), "mode mismatch") {
t.Fatalf("Preflight() error = %v, want mode mismatch", err)
}
}
type preflightArchiveSpec struct {
installationID string
schemaVersion int
@@ -243,12 +328,15 @@ type preflightArchiveSpec struct {
}
type preflightArchiveEntry struct {
path string
body []byte
checksum string
kind string
sensitive bool
symlink bool
path string
body []byte
checksum string
kind string
sensitive bool
symlink bool
mode os.FileMode
manifestMode *uint32
method uint16
}
type preflightRawArchiveEntry struct {
@@ -302,7 +390,14 @@ func writePreflightArchive(t *testing.T, archivePath string, spec preflightArchi
sourcePath = "/protected/secret"
owner = "external-secret"
}
manifest.Entries = append(manifest.Entries, Entry{Path: entry.path, Kind: kind, Owner: owner, SourcePath: sourcePath, SHA256: checksum, Size: int64(len(entry.body)), Archived: true, Sensitive: entry.sensitive})
mode := uint32(entry.mode.Perm())
if mode == 0 {
mode = 0o600
}
if entry.manifestMode != nil {
mode = *entry.manifestMode
}
manifest.Entries = append(manifest.Entries, Entry{Path: entry.path, Kind: kind, Owner: owner, SourcePath: sourcePath, SHA256: checksum, Size: int64(len(entry.body)), Mode: mode, Archived: true, Sensitive: entry.sensitive})
}
manifestBytes, err := manifest.JSON()
if err != nil {
@@ -322,7 +417,16 @@ func writePreflightArchive(t *testing.T, archivePath string, spec preflightArchi
defer file.Close()
writer := zip.NewWriter(file)
for _, entry := range spec.entries {
header := &zip.FileHeader{Name: entry.path, Method: zip.Store}
method := entry.method
if method == 0 {
method = zip.Store
}
header := &zip.FileHeader{Name: entry.path, Method: method}
mode := entry.mode
if mode == 0 {
mode = 0o600
}
header.SetMode(mode)
if entry.symlink {
header.SetMode(os.ModeSymlink | 0o777)
}
@@ -343,7 +447,9 @@ func writePreflightArchive(t *testing.T, archivePath string, spec preflightArchi
t.Fatal(err)
}
}
created, err := writer.Create(ManifestPath)
manifestHeader := &zip.FileHeader{Name: ManifestPath, Method: zip.Store}
manifestHeader.SetMode(0o600)
created, err := writer.CreateHeader(manifestHeader)
if err != nil {
t.Fatal(err)
}