Skip to content

Commit cf15d84

Browse files
committed
Add decompression bomb mitigation options
1 parent 611343a commit cf15d84

16 files changed

+283
-33
lines changed

decompress_bzip2.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import (
99

1010
// Bzip2Decompressor is an implementation of Decompressor that can
1111
// decompress bz2 files.
12-
type Bzip2Decompressor struct{}
12+
type Bzip2Decompressor struct {
13+
// FileSizeLimit limits the size of a decompressed file.
14+
//
15+
// The zero value means no limit.
16+
FileSizeLimit int64
17+
}
1318

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

3540
// Copy it out
36-
return copyReader(dst, bzipR, 0622, umask)
41+
return copyReader(dst, bzipR, 0622, umask, d.FileSizeLimit)
3742
}

decompress_gzip.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import (
99

1010
// GzipDecompressor is an implementation of Decompressor that can
1111
// decompress gzip files.
12-
type GzipDecompressor struct{}
12+
type GzipDecompressor struct {
13+
// FileSizeLimit limits the size of a decompressed file.
14+
//
15+
// The zero value means no limit.
16+
FileSizeLimit int64
17+
}
1318

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

3944
// Copy it out
40-
return copyReader(dst, gzipR, 0622, umask)
45+
return copyReader(dst, gzipR, 0622, umask, d.FileSizeLimit)
4146
}

decompress_tar.go

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,25 @@ import (
1111

1212
// untar is a shared helper for untarring an archive. The reader should provide
1313
// an uncompressed view of the tar archive.
14-
func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode) error {
14+
func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode, fileSizeLimit int64, filesLimit int) error {
1515
tarR := tar.NewReader(input)
1616
done := false
1717
dirHdrs := []*tar.Header{}
1818
now := time.Now()
19+
20+
var (
21+
fileSize int64
22+
filesCount int
23+
)
24+
1925
for {
26+
if filesLimit > 0 {
27+
filesCount++
28+
if filesCount > filesLimit {
29+
return fmt.Errorf("tar archive contains too many files: %d > %d", filesCount, filesLimit)
30+
}
31+
}
32+
2033
hdr, err := tarR.Next()
2134
if err == io.EOF {
2235
if !done {
@@ -45,7 +58,15 @@ func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode) error
4558
path = filepath.Join(path, hdr.Name)
4659
}
4760

48-
if hdr.FileInfo().IsDir() {
61+
fileInfo := hdr.FileInfo()
62+
63+
fileSize += fileInfo.Size()
64+
65+
if fileSizeLimit > 0 && fileSize > fileSizeLimit {
66+
return fmt.Errorf("tar archive larger than limit: %d", fileSizeLimit)
67+
}
68+
69+
if fileInfo.IsDir() {
4970
if !dir {
5071
return fmt.Errorf("expected a single file: %s", src)
5172
}
@@ -81,8 +102,8 @@ func untar(input io.Reader, dst, src string, dir bool, umask os.FileMode) error
81102
// Mark that we're done so future in single file mode errors
82103
done = true
83104

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

128149
// TarDecompressor is an implementation of Decompressor that can
129150
// unpack tar files.
130-
type TarDecompressor struct{}
151+
type TarDecompressor struct {
152+
// FileSizeLimit limits the total size of all
153+
// decompressed files.
154+
//
155+
// The zero value means no limit.
156+
FileSizeLimit int64
157+
158+
// FilesLimit limits the number of files that are
159+
// allowed to be decompressed.
160+
//
161+
// The zero value means no limit.
162+
FilesLimit int
163+
}
131164

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

149-
return untar(f, dst, src, dir, umask)
182+
return untar(f, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
150183
}

decompress_tar_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package getter
22

33
import (
4+
"archive/tar"
5+
"bytes"
46
"io/ioutil"
57
"os"
68
"path/filepath"
79
"runtime"
10+
"strings"
811
"testing"
912
"time"
1013
)
@@ -45,6 +48,86 @@ func TestTar(t *testing.T) {
4548
TestDecompressor(t, new(TarDecompressor), cases)
4649
}
4750

51+
func TestTarLimits(t *testing.T) {
52+
b := bytes.NewBuffer(nil)
53+
54+
tw := tar.NewWriter(b)
55+
56+
var files = []struct {
57+
Name, Body string
58+
}{
59+
{"readme.txt", "This archive contains some text files."},
60+
{"gopher.txt", "Gopher names:\nCharlie\nRonald\nGlenn"},
61+
{"todo.txt", "Get animal handling license."},
62+
}
63+
64+
for _, file := range files {
65+
hdr := &tar.Header{
66+
Name: file.Name,
67+
Mode: 0600,
68+
Size: int64(len(file.Body)),
69+
}
70+
if err := tw.WriteHeader(hdr); err != nil {
71+
t.Fatal(err)
72+
}
73+
if _, err := tw.Write([]byte(file.Body)); err != nil {
74+
t.Fatal(err)
75+
}
76+
}
77+
78+
if err := tw.Close(); err != nil {
79+
t.Fatal(err)
80+
}
81+
82+
td, err := ioutil.TempDir("", "getter")
83+
if err != nil {
84+
t.Fatalf("err: %s", err)
85+
}
86+
87+
tarFilePath := filepath.Join(td, "input.tar")
88+
89+
err = os.WriteFile(tarFilePath, b.Bytes(), 0666)
90+
if err != nil {
91+
t.Fatalf("err: %s", err)
92+
}
93+
94+
t.Run("file size limit", func(t *testing.T) {
95+
d := new(TarDecompressor)
96+
97+
d.FileSizeLimit = 35
98+
99+
dst := filepath.Join(td, "subdir", "file-size-limit-result")
100+
101+
err = d.Decompress(dst, tarFilePath, true, 0022)
102+
103+
if err == nil {
104+
t.Fatal("expected file size limit to error")
105+
}
106+
107+
if !strings.Contains(err.Error(), "tar archive larger than limit: 35") {
108+
t.Fatalf("unexpected error: %q", err.Error())
109+
}
110+
})
111+
112+
t.Run("files limit", func(t *testing.T) {
113+
d := new(TarDecompressor)
114+
115+
d.FilesLimit = 2
116+
117+
dst := filepath.Join(td, "subdir", "files-limit-result")
118+
119+
err = d.Decompress(dst, tarFilePath, true, 0022)
120+
121+
if err == nil {
122+
t.Fatal("expected files limit to error")
123+
}
124+
125+
if !strings.Contains(err.Error(), "tar archive contains too many files: 3 > 2") {
126+
t.Fatalf("unexpected error: %q", err.Error())
127+
}
128+
})
129+
}
130+
48131
// testDecompressPermissions decompresses a directory and checks the permissions of the expanded files
49132
func testDecompressorPermissions(t *testing.T, d Decompressor, input string, expected map[string]int, umask os.FileMode) {
50133
td, err := ioutil.TempDir("", "getter")

decompress_tbz2.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@ import (
88

99
// TarBzip2Decompressor is an implementation of Decompressor that can
1010
// decompress tar.bz2 files.
11-
type TarBzip2Decompressor struct{}
11+
type TarBzip2Decompressor struct {
12+
// FileSizeLimit limits the total size of all
13+
// decompressed files.
14+
//
15+
// The zero value means no limit.
16+
FileSizeLimit int64
17+
18+
// FilesLimit limits the number of files that are
19+
// allowed to be decompressed.
20+
//
21+
// The zero value means no limit.
22+
FilesLimit int
23+
}
1224

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

3042
// Bzip2 compression is second
3143
bzipR := bzip2.NewReader(f)
32-
return untar(bzipR, dst, src, dir, umask)
44+
return untar(bzipR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
3345
}

decompress_tgz.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,19 @@ import (
99

1010
// TarGzipDecompressor is an implementation of Decompressor that can
1111
// decompress tar.gzip files.
12-
type TarGzipDecompressor struct{}
12+
type TarGzipDecompressor struct {
13+
// FileSizeLimit limits the total size of all
14+
// decompressed files.
15+
//
16+
// The zero value means no limit.
17+
FileSizeLimit int64
18+
19+
// FilesLimit limits the number of files that are
20+
// allowed to be decompressed.
21+
//
22+
// The zero value means no limit.
23+
FilesLimit int
24+
}
1325

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

38-
return untar(gzipR, dst, src, dir, umask)
50+
return untar(gzipR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
3951
}

decompress_txz.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ import (
1010

1111
// TarXzDecompressor is an implementation of Decompressor that can
1212
// decompress tar.xz files.
13-
type TarXzDecompressor struct{}
13+
type TarXzDecompressor struct {
14+
// FileSizeLimit limits the total size of all
15+
// decompressed files.
16+
//
17+
// The zero value means no limit.
18+
FileSizeLimit int64
19+
20+
// FilesLimit limits the number of files that are
21+
// allowed to be decompressed.
22+
//
23+
// The zero value means no limit.
24+
FilesLimit int
25+
}
1426

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

38-
return untar(txzR, dst, src, dir, umask)
50+
return untar(txzR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
3951
}

decompress_tzst.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,27 @@ package getter
22

33
import (
44
"fmt"
5-
"github.com/klauspost/compress/zstd"
65
"os"
76
"path/filepath"
7+
8+
"github.com/klauspost/compress/zstd"
89
)
910

1011
// TarZstdDecompressor is an implementation of Decompressor that can
1112
// decompress tar.zstd files.
12-
type TarZstdDecompressor struct{}
13+
type TarZstdDecompressor struct {
14+
// FileSizeLimit limits the total size of all
15+
// decompressed files.
16+
//
17+
// The zero value means no limit.
18+
FileSizeLimit int64
19+
20+
// FilesLimit limits the number of files that are
21+
// allowed to be decompressed.
22+
//
23+
// The zero value means no limit.
24+
FilesLimit int
25+
}
1326

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

38-
return untar(zstdR, dst, src, dir, umask)
51+
return untar(zstdR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)
3952
}

decompress_xz.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import (
1010

1111
// XzDecompressor is an implementation of Decompressor that can
1212
// decompress xz files.
13-
type XzDecompressor struct{}
13+
type XzDecompressor struct {
14+
// FileSizeLimit limits the size of a decompressed file.
15+
//
16+
// The zero value means no limit.
17+
FileSizeLimit int64
18+
}
1419

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

39-
// Copy it out
40-
return copyReader(dst, xzR, 0622, umask)
44+
// Copy it out, potentially using a file size limit.
45+
return copyReader(dst, xzR, 0622, umask, d.FileSizeLimit)
4146
}

0 commit comments

Comments
 (0)