package services import ( "archive/zip" "bytes" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) func openZipInTempDir(t *testing.T, tempDir, zipPath string) *os.File { t.Helper() absTempDir, err := filepath.Abs(tempDir) require.NoError(t, err) absZipPath, err := filepath.Abs(zipPath) require.NoError(t, err) relPath, err := filepath.Rel(absTempDir, absZipPath) require.NoError(t, err) require.False(t, relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator))) // #nosec G304 -- absZipPath is constrained to test TempDir via Abs+Rel checks above. zipFile, err := os.OpenFile(absZipPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) require.NoError(t, err) return zipFile } func TestBackupService_UnzipWithSkip_SkipsDatabaseEntries(t *testing.T) { tmp := t.TempDir() destDir := filepath.Join(tmp, "data") require.NoError(t, os.MkdirAll(destDir, 0o700)) zipPath := filepath.Join(tmp, "restore.zip") zipFile := openZipInTempDir(t, tmp, zipPath) writer := zip.NewWriter(zipFile) for name, content := range map[string]string{ "charon.db": "db", "charon.db-wal": "wal", "charon.db-shm": "shm", "caddy/config": "cfg", "nested/file.txt": "hello", } { entry, createErr := writer.Create(name) require.NoError(t, createErr) _, writeErr := entry.Write([]byte(content)) require.NoError(t, writeErr) } require.NoError(t, writer.Close()) require.NoError(t, zipFile.Close()) svc := &BackupService{DataDir: destDir, DatabaseName: "charon.db"} require.NoError(t, svc.unzipWithSkip(zipPath, destDir, map[string]struct{}{ "charon.db": {}, "charon.db-wal": {}, "charon.db-shm": {}, })) _, err := os.Stat(filepath.Join(destDir, "charon.db")) require.Error(t, err) require.FileExists(t, filepath.Join(destDir, "caddy", "config")) require.FileExists(t, filepath.Join(destDir, "nested", "file.txt")) } func TestBackupService_ExtractDatabaseFromBackup_ExtractWalFailure(t *testing.T) { tmp := t.TempDir() zipPath := filepath.Join(tmp, "invalid-wal.zip") zipFile := openZipInTempDir(t, tmp, zipPath) writer := zip.NewWriter(zipFile) dbEntry, err := writer.Create("charon.db") require.NoError(t, err) _, err = dbEntry.Write([]byte("sqlite header placeholder")) require.NoError(t, err) walEntry, err := writer.Create("charon.db-wal") require.NoError(t, err) _, err = walEntry.Write([]byte("invalid wal content")) require.NoError(t, err) require.NoError(t, writer.Close()) require.NoError(t, zipFile.Close()) svc := &BackupService{DatabaseName: "charon.db"} _, err = svc.extractDatabaseFromBackup(zipPath) require.Error(t, err) } func TestBackupService_UnzipWithSkip_RejectsPathTraversal(t *testing.T) { tmp := t.TempDir() destDir := filepath.Join(tmp, "data") require.NoError(t, os.MkdirAll(destDir, 0o700)) zipPath := filepath.Join(tmp, "path-traversal.zip") zipFile := openZipInTempDir(t, tmp, zipPath) writer := zip.NewWriter(zipFile) entry, err := writer.Create("../escape.txt") require.NoError(t, err) _, err = entry.Write([]byte("evil")) require.NoError(t, err) require.NoError(t, writer.Close()) require.NoError(t, zipFile.Close()) svc := &BackupService{DataDir: destDir, DatabaseName: "charon.db"} err = svc.unzipWithSkip(zipPath, destDir, nil) require.Error(t, err) require.Contains(t, err.Error(), "invalid file path in archive") } func TestBackupService_UnzipWithSkip_RejectsExcessiveUncompressedSize(t *testing.T) { tmp := t.TempDir() destDir := filepath.Join(tmp, "data") require.NoError(t, os.MkdirAll(destDir, 0o700)) zipPath := filepath.Join(tmp, "oversized.zip") zipFile := openZipInTempDir(t, tmp, zipPath) writer := zip.NewWriter(zipFile) entry, err := writer.Create("huge.bin") require.NoError(t, err) _, err = entry.Write(bytes.Repeat([]byte("a"), 101*1024*1024)) require.NoError(t, err) require.NoError(t, writer.Close()) require.NoError(t, zipFile.Close()) svc := &BackupService{DataDir: destDir, DatabaseName: "charon.db"} err = svc.unzipWithSkip(zipPath, destDir, nil) require.Error(t, err) require.Contains(t, err.Error(), "exceeded decompression limit") }