From 1b3d8dc59cb5f2b759ab0ad2bc9915b993e3cb73 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 24 Feb 2026 20:39:49 +0100 Subject: [PATCH] fix(restore): pre-validate all table data JSON before wiping database --- pkg/modules/dump/restore.go | 40 +++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/modules/dump/restore.go b/pkg/modules/dump/restore.go index c1334bd10..14742e649 100644 --- a/pkg/modules/dump/restore.go +++ b/pkg/modules/dump/restore.go @@ -48,6 +48,19 @@ import ( const maxConfigSize = 5 * 1024 * 1024 // 5 MB, should be largely enough const maxDumpEntrySize = 500 * 1024 * 1024 // 500 MB +// parseDbFileName validates and extracts the table name from a database dump filename. +// Returns the table name and true if valid, or empty string and false if invalid. +func parseDbFileName(fname string) (string, bool) { + if !strings.HasSuffix(fname, ".json") { + return "", false + } + tableName := strings.TrimSuffix(fname, ".json") + if tableName == "" || strings.ContainsAny(tableName, "/\\") { + return "", false + } + return tableName, true +} + // Restore takes a zip file name and restores it func Restore(filename string, overrideConfig bool) error { @@ -84,10 +97,11 @@ func Restore(filename string, overrideConfig bool) error { } if strings.HasPrefix(file.Name, "database/") { fname := strings.TrimPrefix(file.Name, "database/") - if !strings.HasSuffix(fname, ".json") || len(fname) <= 5 { + tableName, valid := parseDbFileName(fname) + if !valid { return fmt.Errorf("invalid database file name in zip archive: %q", file.Name) } - dbfiles[fname[:len(fname)-5]] = file + dbfiles[tableName] = file continue } if file.Name == ".env" { @@ -166,6 +180,28 @@ func Restore(filename string, overrideConfig bool) error { lastMigration := ms[len(ms)-2] + // Pre-validate all table data JSON before wiping to avoid leaving the database + // in a destroyed state when the archive contains corrupted table data. + for table, d := range dbfiles { + if table == "migration" { + continue + } + rc, err := d.Open() + if err != nil { + return fmt.Errorf("could not open table data for %s: %w", table, err) + } + var bufValidate bytes.Buffer + if _, err := bufValidate.ReadFrom(io.LimitReader(rc, maxDumpEntrySize)); err != nil { + rc.Close() + return fmt.Errorf("could not read table data for %s: %w", table, err) + } + rc.Close() + var test []map[string]interface{} + if err := json.Unmarshal(bufValidate.Bytes(), &test); err != nil { + return fmt.Errorf("invalid JSON in table data for %s: %w", table, err) + } + } + // Start by wiping everything - only after we've validated the archive if err := db.WipeEverything(); err != nil { return fmt.Errorf("could not wipe database: %w", err)