Add cap for backup data extraction (#10260)

* add cap for backup data extraction

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

* set default extraction size

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

* control total size only

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

* add doc for max-backup-extraction-size

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>

---------

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>
This commit is contained in:
lyndon-li
2026-08-17 16:29:42 +08:00
committed by GitHub
parent adc35b635c
commit d4e62bb979
6 changed files with 116 additions and 4 deletions
+26 -4
View File
@@ -32,14 +32,27 @@ import (
// Extractor unzips/extracts a backup tarball to a local
// temp directory.
type Extractor struct {
log logrus.FieldLogger
fs filesystem.Interface
log logrus.FieldLogger
fs filesystem.Interface
maxExtractionSize int64
totalExtractedSize int64
}
var maxExtractionSize = int64(16) << 30
// SetMaxExtractionSize sets the maximum extraction size. It is normally called at server startup.
func SetMaxExtractionSize(size int64) {
if size > 0 {
maxExtractionSize = size
}
}
func NewExtractor(log logrus.FieldLogger, fs filesystem.Interface) *Extractor {
return &Extractor{
log: log,
fs: fs,
log: log,
fs: fs,
maxExtractionSize: maxExtractionSize,
totalExtractedSize: 0,
}
}
@@ -96,6 +109,15 @@ func (e *Extractor) readBackup(tarRdr *tar.Reader) (string, error) {
return "", err
}
// Enforce maximum extraction size to prevent memory/storage exhaustion and zip bombs.
maxSize := e.maxExtractionSize
e.totalExtractedSize += header.Size
if e.totalExtractedSize > maxSize {
err := fmt.Errorf("decompressed backup exceeds maximum allowed size of %d bytes", maxSize)
e.log.Infof("error checking extracted size: %v", err)
return "", err
}
target, err := sanitizeArchivePath(dir, header.Name)
if err != nil {
e.log.Infof("error sanitizing archive path: %s", err.Error())
+61
View File
@@ -20,6 +20,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"testing"
@@ -113,6 +114,66 @@ func TestUnzipAndExtractBackupRejectsPathTraversal(t *testing.T) {
require.Contains(t, err.Error(), "invalid archive path")
}
func TestUnzipAndExtractBackupRejectsLargeFile(t *testing.T) {
SetMaxExtractionSize(1024)
defer SetMaxExtractionSize(16 * 1024 * 1024 * 1024)
ext := NewExtractor(test.NewLogger(), test.NewFakeFileSystem())
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gzw)
data := make([]byte, 2048) // 2KB data
err := tw.WriteHeader(&tar.Header{
Name: "large.txt",
Mode: 0600,
Typeflag: tar.TypeReg,
Size: int64(len(data)),
})
require.NoError(t, err)
_, err = tw.Write(data)
require.NoError(t, err)
require.NoError(t, tw.Close())
require.NoError(t, gzw.Close())
_, err = ext.UnzipAndExtractBackup(&buf)
require.Error(t, err)
require.Contains(t, err.Error(), "decompressed backup exceeds maximum allowed size")
}
func TestUnzipAndExtractBackupRejectsManySmallFiles(t *testing.T) {
SetMaxExtractionSize(1024)
defer SetMaxExtractionSize(16 * 1024 * 1024 * 1024)
ext := NewExtractor(test.NewLogger(), test.NewFakeFileSystem())
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gzw)
// Create 100 files of 20 bytes each (total 2000 bytes, exceeding the 1024 byte limit)
for i := 0; i < 100; i++ {
data := make([]byte, 20)
err := tw.WriteHeader(&tar.Header{
Name: fmt.Sprintf("small_%d.txt", i),
Mode: 0600,
Typeflag: tar.TypeReg,
Size: int64(len(data)),
})
require.NoError(t, err)
_, err = tw.Write(data)
require.NoError(t, err)
}
require.NoError(t, tw.Close())
require.NoError(t, gzw.Close())
_, err := ext.UnzipAndExtractBackup(&buf)
require.Error(t, err)
require.Contains(t, err.Error(), "decompressed backup exceeds maximum allowed size")
}
func createArchive(files []string, fs filesystem.Interface) (string, error) {
outName := "output.tar.gz"
out, err := fs.Create(outName)
+7
View File
@@ -183,6 +183,7 @@ type Config struct {
ConcurrentBackups int
GlobalBackupVolumePoliciesConfigMap string
DefaultResourceModifierConfigMap string
MaxBackupExtractionSize int
}
func GetDefaultConfig() *Config {
@@ -289,4 +290,10 @@ func (c *Config) BindFlags(flags *pflag.FlagSet) {
c.DefaultResourceModifierConfigMap,
"The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores. Ignored when a per-restore resource modifier is specified.",
)
flags.IntVar(
&c.MaxBackupExtractionSize,
"max-backup-extraction-size",
c.MaxBackupExtractionSize,
"Maximum size of a backup extraction in megabytes. If not set, default value (16GB) will be used.",
)
}
+6
View File
@@ -61,6 +61,7 @@ import (
"github.com/vmware-tanzu/velero/internal/storage"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
"github.com/vmware-tanzu/velero/pkg/archive"
"github.com/vmware-tanzu/velero/pkg/backup"
"github.com/vmware-tanzu/velero/pkg/buildinfo"
"github.com/vmware-tanzu/velero/pkg/client"
@@ -935,6 +936,11 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
}
}
if s.config.MaxBackupExtractionSize > 0 {
s.logger.Infof("Setting backup data extraction cap as %v MB", s.config.MaxBackupExtractionSize)
archive.SetMaxExtractionSize(int64(s.config.MaxBackupExtractionSize) * 1024 * 1024)
}
s.logger.Info("Server starting...")
if err := s.mgr.Start(s.ctx); err != nil {