Skip to content

Commit

Permalink
Add decompression bomb mitigation options
Browse files Browse the repository at this point in the history
  • Loading branch information
picatz committed Feb 8, 2023
1 parent 611343a commit cf15d84
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 33 deletions.
9 changes: 7 additions & 2 deletions decompress_bzip2.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import (

// Bzip2Decompressor is an implementation of Decompressor that can
// decompress bz2 files.
type Bzip2Decompressor struct{}
type Bzip2Decompressor struct {
// FileSizeLimit limits the size of a decompressed file.
//
// The zero value means no limit.
FileSizeLimit int64
}

func (d *Bzip2Decompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// Directory isn't supported at all
Expand All @@ -33,5 +38,5 @@ func (d *Bzip2Decompressor) Decompress(dst, src string, dir bool, umask os.FileM
bzipR := bzip2.NewReader(f)

// Copy it out
return copyReader(dst, bzipR, 0622, umask)
return copyReader(dst, bzipR, 0622, umask, d.FileSizeLimit)
}
9 changes: 7 additions & 2 deletions decompress_gzip.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import (

// GzipDecompressor is an implementation of Decompressor that can
// decompress gzip files.
type GzipDecompressor struct{}
type GzipDecompressor struct {
// FileSizeLimit limits the size of a decompressed file.
//
// The zero value means no limit.
FileSizeLimit int64
}

func (d *GzipDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// Directory isn't supported at all
Expand Down Expand Up @@ -37,5 +42,5 @@ func (d *GzipDecompressor) Decompress(dst, src string, dir bool, umask os.FileMo
defer gzipR.Close()

// Copy it out
return copyReader(dst, gzipR, 0622, umask)
return copyReader(dst, gzipR, 0622, umask, d.FileSizeLimit)
}
45 changes: 39 additions & 6 deletions decompress_tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,25 @@ import (

// untar is a shared helper for untarring an archive. The reader should provide
// an uncompressed view of the tar archive.
func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode) error {
func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode, fileSizeLimit int64, filesLimit int) error {
tarR := tar.NewReader(input)
done := false
dirHdrs := []*tar.Header{}
now := time.Now()

var (
fileSize int64
filesCount int
)

for {
if filesLimit > 0 {
filesCount++
if filesCount > filesLimit {
return fmt.Errorf("tar archive contains too many files: %d > %d", filesCount, filesLimit)
}
}

hdr, err := tarR.Next()
if err == io.EOF {
if !done {
Expand Down Expand Up @@ -45,7 +58,15 @@ func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode) error
path = filepath.Join(path, hdr.Name)
}

if hdr.FileInfo().IsDir() {
fileInfo := hdr.FileInfo()

fileSize += fileInfo.Size()

if fileSizeLimit > 0 && fileSize > fileSizeLimit {
return fmt.Errorf("tar archive larger than limit: %d", fileSizeLimit)
}

if fileInfo.IsDir() {
if !dir {
return fmt.Errorf("expected a single file: %s", src)
}
Expand Down Expand Up @@ -81,8 +102,8 @@ func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode) error
// Mark that we're done so future in single file mode errors
done = true

// Open the file for writing
err = copyReader(path, tarR, hdr.FileInfo().Mode(), umask)
// Size limit is tracked using the returned file info.
err = copyReader(path, tarR, hdr.FileInfo().Mode(), umask, 0)
if err != nil {
return err
}
Expand Down Expand Up @@ -127,7 +148,19 @@ func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode) error

// TarDecompressor is an implementation of Decompressor that can
// unpack tar files.
type TarDecompressor struct{}
type TarDecompressor struct {
// FileSizeLimit limits the total size of all
// decompressed files.
//
// The zero value means no limit.
FileSizeLimit int64

// FilesLimit limits the number of files that are
// allowed to be decompressed.
//
// The zero value means no limit.
FilesLimit int
}

func (d *TarDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// If we're going into a directory we should make that first
Expand All @@ -146,5 +179,5 @@ func (d *TarDecompressor) Decompress(dst, src string, dir bool, umask os.FileMod
}
defer f.Close()

return untar(f, dst, src, dir, umask)
return untar(f, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
}
83 changes: 83 additions & 0 deletions decompress_tar_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package getter

import (
"archive/tar"
"bytes"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -45,6 +48,86 @@ func TestTar(t *testing.T) {
TestDecompressor(t, new(TarDecompressor), cases)
}

func TestTarLimits(t *testing.T) {
b := bytes.NewBuffer(nil)

tw := tar.NewWriter(b)

var files = []struct {
Name, Body string
}{
{"readme.txt", "This archive contains some text files."},
{"gopher.txt", "Gopher names:\nCharlie\nRonald\nGlenn"},
{"todo.txt", "Get animal handling license."},
}

for _, file := range files {
hdr := &tar.Header{
Name: file.Name,
Mode: 0600,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatal(err)
}
}

if err := tw.Close(); err != nil {
t.Fatal(err)
}

td, err := ioutil.TempDir("", "getter")
if err != nil {
t.Fatalf("err: %s", err)
}

tarFilePath := filepath.Join(td, "input.tar")

err = os.WriteFile(tarFilePath, b.Bytes(), 0666)
if err != nil {
t.Fatalf("err: %s", err)
}

t.Run("file size limit", func(t *testing.T) {
d := new(TarDecompressor)

d.FileSizeLimit = 35

dst := filepath.Join(td, "subdir", "file-size-limit-result")

err = d.Decompress(dst, tarFilePath, true, 0022)

if err == nil {
t.Fatal("expected file size limit to error")
}

if !strings.Contains(err.Error(), "tar archive larger than limit: 35") {
t.Fatalf("unexpected error: %q", err.Error())
}
})

t.Run("files limit", func(t *testing.T) {
d := new(TarDecompressor)

d.FilesLimit = 2

dst := filepath.Join(td, "subdir", "files-limit-result")

err = d.Decompress(dst, tarFilePath, true, 0022)

if err == nil {
t.Fatal("expected files limit to error")
}

if !strings.Contains(err.Error(), "tar archive contains too many files: 3 > 2") {
t.Fatalf("unexpected error: %q", err.Error())
}
})
}

// testDecompressPermissions decompresses a directory and checks the permissions of the expanded files
func testDecompressorPermissions(t *testing.T, d Decompressor, input string, expected map[string]int, umask os.FileMode) {
td, err := ioutil.TempDir("", "getter")
Expand Down
16 changes: 14 additions & 2 deletions decompress_tbz2.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ import (

// TarBzip2Decompressor is an implementation of Decompressor that can
// decompress tar.bz2 files.
type TarBzip2Decompressor struct{}
type TarBzip2Decompressor struct {
// FileSizeLimit limits the total size of all
// decompressed files.
//
// The zero value means no limit.
FileSizeLimit int64

// FilesLimit limits the number of files that are
// allowed to be decompressed.
//
// The zero value means no limit.
FilesLimit int
}

func (d *TarBzip2Decompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// If we're going into a directory we should make that first
Expand All @@ -29,5 +41,5 @@ func (d *TarBzip2Decompressor) Decompress(dst, src string, dir bool, umask os.Fi

// Bzip2 compression is second
bzipR := bzip2.NewReader(f)
return untar(bzipR, dst, src, dir, umask)
return untar(bzipR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
}
16 changes: 14 additions & 2 deletions decompress_tgz.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ import (

// TarGzipDecompressor is an implementation of Decompressor that can
// decompress tar.gzip files.
type TarGzipDecompressor struct{}
type TarGzipDecompressor struct {
// FileSizeLimit limits the total size of all
// decompressed files.
//
// The zero value means no limit.
FileSizeLimit int64

// FilesLimit limits the number of files that are
// allowed to be decompressed.
//
// The zero value means no limit.
FilesLimit int
}

func (d *TarGzipDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// If we're going into a directory we should make that first
Expand All @@ -35,5 +47,5 @@ func (d *TarGzipDecompressor) Decompress(dst, src string, dir bool, umask os.Fil
}
defer gzipR.Close()

return untar(gzipR, dst, src, dir, umask)
return untar(gzipR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
}
16 changes: 14 additions & 2 deletions decompress_txz.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ import (

// TarXzDecompressor is an implementation of Decompressor that can
// decompress tar.xz files.
type TarXzDecompressor struct{}
type TarXzDecompressor struct {
// FileSizeLimit limits the total size of all
// decompressed files.
//
// The zero value means no limit.
FileSizeLimit int64

// FilesLimit limits the number of files that are
// allowed to be decompressed.
//
// The zero value means no limit.
FilesLimit int
}

func (d *TarXzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// If we're going into a directory we should make that first
Expand All @@ -35,5 +47,5 @@ func (d *TarXzDecompressor) Decompress(dst, src string, dir bool, umask os.FileM
return fmt.Errorf("Error opening an xz reader for %s: %s", src, err)
}

return untar(txzR, dst, src, dir, umask)
return untar(txzR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
}
19 changes: 16 additions & 3 deletions decompress_tzst.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ package getter

import (
"fmt"
"github.com/klauspost/compress/zstd"
"os"
"path/filepath"

"github.com/klauspost/compress/zstd"
)

// TarZstdDecompressor is an implementation of Decompressor that can
// decompress tar.zstd files.
type TarZstdDecompressor struct{}
type TarZstdDecompressor struct {
// FileSizeLimit limits the total size of all
// decompressed files.
//
// The zero value means no limit.
FileSizeLimit int64

// FilesLimit limits the number of files that are
// allowed to be decompressed.
//
// The zero value means no limit.
FilesLimit int
}

func (d *TarZstdDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// If we're going into a directory we should make that first
Expand All @@ -35,5 +48,5 @@ func (d *TarZstdDecompressor) Decompress(dst, src string, dir bool, umask os.Fil
}
defer zstdR.Close()

return untar(zstdR, dst, src, dir, umask)
return untar(zstdR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
}
11 changes: 8 additions & 3 deletions decompress_xz.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import (

// XzDecompressor is an implementation of Decompressor that can
// decompress xz files.
type XzDecompressor struct{}
type XzDecompressor struct {
// FileSizeLimit limits the size of a decompressed file.
//
// The zero value means no limit.
FileSizeLimit int64
}

func (d *XzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {
// Directory isn't supported at all
Expand All @@ -36,6 +41,6 @@ func (d *XzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode
return err
}

// Copy it out
return copyReader(dst, xzR, 0622, umask)
// Copy it out, potentially using a file size limit.
return copyReader(dst, xzR, 0622, umask, d.FileSizeLimit)
}
Loading

0 comments on commit cf15d84

Please sign in to comment.