◐ Shell
clean mode source ↗

fix: reject oversized and invalid zip uploads (#25877) (#26179) · coder/coder@430ba84

@@ -6,43 +6,153 @@ import (

66

"bytes"

77

"errors"

88

"io"

9-

"log"

9+

"math"

1010

"strings"

11+12+

"golang.org/x/xerrors"

13+

)

14+15+

// Ref:

16+

// https://github.com/golang/go/blob/go1.24.0/src/archive/tar/format.go

17+

// https://github.com/golang/go/blob/go1.24.0/src/archive/tar/writer.go

18+

const (

19+

tarBlockSize = 512

20+

tarEndBlockBytes = 2 * tarBlockSize

1121

)

122223+

// ErrArchiveTooLarge reports that archive expansion would exceed the

24+

// configured limit.

25+

var ErrArchiveTooLarge = xerrors.New("archive exceeds maximum size")

26+27+

// ErrInvalidZipContent reports that a ZIP entry is malformed or its

28+

// contents fail validation during conversion.

29+

var ErrInvalidZipContent = xerrors.New("invalid zip content")

30+1331

// CreateTarFromZip converts the given zipReader to a tar archive.

32+

// maxSize limits the total tar output, including tar metadata.

1433

func CreateTarFromZip(zipReader *zip.Reader, maxSize int64) ([]byte, error) {

34+

err := validateZipArchiveSize(zipReader, maxSize)

35+

if err != nil {

36+

return nil, err

37+

}

38+1539

var tarBuffer bytes.Buffer

16-

err := writeTarArchive(&tarBuffer, zipReader, maxSize)

40+

err = writeTarArchive(&tarBuffer, zipReader, maxSize)

1741

if err != nil {

1842

return nil, err

1943

}

2044

return tarBuffer.Bytes(), nil

2145

}

224623-

func writeTarArchive(w io.Writer, zipReader *zip.Reader, maxSize int64) error {

24-

tarWriter := tar.NewWriter(w)

25-

defer tarWriter.Close()

47+

// validateZipArchiveSize performs a metadata-based preflight size

48+

// check before conversion. The actual tar output limit will still be

49+

// enforced while streaming.

50+

func validateZipArchiveSize(zipReader *zip.Reader, maxSize int64) error {

51+

if maxSize < 0 {

52+

return ErrArchiveTooLarge

53+

}

54+55+

maxBytes := uint64(maxSize)

56+

totalBytes := uint64(tarEndBlockBytes)

57+

if totalBytes > maxBytes {

58+

return ErrArchiveTooLarge

59+

}

26602761

for _, file := range zipReader.File {

28-

err := processFileInZipArchive(file, tarWriter, maxSize)

62+

entrySize, err := projectedTarEntrySize(file)

2963

if err != nil {

3064

return err

3165

}

66+

if entrySize > maxBytes-totalBytes {

67+

return ErrArchiveTooLarge

68+

}

69+

totalBytes += entrySize

3270

}

71+3372

return nil

3473

}

357436-

func processFileInZipArchive(file *zip.File, tarWriter *tar.Writer, maxSize int64) error {

75+

func projectedTarEntrySize(file *zip.File) (uint64, error) {

76+

// Each tar entry contributes one header block plus its data

77+

// rounded up to the next tar block boundary.

78+

size := file.UncompressedSize64

79+

if remainder := size % tarBlockSize; remainder != 0 {

80+

padding := tarBlockSize - remainder

81+

if size > math.MaxUint64-padding {

82+

return 0, ErrArchiveTooLarge

83+

}

84+

size += padding

85+

}

86+87+

if size > math.MaxUint64-tarBlockSize {

88+

return 0, ErrArchiveTooLarge

89+

}

90+91+

return tarBlockSize + size, nil

92+

}

93+94+

type limitedWriter struct {

95+

w io.Writer

96+

remaining int64

97+

}

98+99+

func (w *limitedWriter) Write(p []byte) (int, error) {

100+

if len(p) == 0 {

101+

return 0, nil

102+

}

103+

if w.remaining <= 0 {

104+

return 0, ErrArchiveTooLarge

105+

}

106+107+

origLen := len(p)

108+

if int64(origLen) > w.remaining {

109+

p = p[:int(w.remaining)]

110+

}

111+112+

n, err := w.w.Write(p)

113+

// io.Writer may report both written bytes and an error, so

114+

// account for any accepted bytes before returning the error.

115+

w.remaining -= int64(n)

116+

if err != nil {

117+

return n, err

118+

}

119+

if n < origLen {

120+

return n, ErrArchiveTooLarge

121+

}

122+

return n, nil

123+

}

124+125+

func writeTarArchive(w io.Writer, zipReader *zip.Reader, maxSize int64) error {

126+

tarWriter := tar.NewWriter(&limitedWriter{

127+

w: w,

128+

remaining: maxSize,

129+

})

130+131+

for _, file := range zipReader.File {

132+

err := processFileInZipArchive(file, tarWriter)

133+

if err != nil {

134+

return err

135+

}

136+

}

137+138+

return tarWriter.Close()

139+

}

140+141+

func processFileInZipArchive(file *zip.File, tarWriter *tar.Writer) error {

37142

fileReader, err := file.Open()

38143

if err != nil {

39144

return err

40145

}

41146

defer fileReader.Close()

42147148+

size := file.FileInfo().Size()

149+

if size < 0 {

150+

return ErrArchiveTooLarge

151+

}

152+43153

err = tarWriter.WriteHeader(&tar.Header{

44154

Name: file.Name,

45-

Size: file.FileInfo().Size(),

155+

Size: size,

46156

Mode: int64(file.Mode()),

47157

ModTime: file.Modified,

48158

// Note: Zip archives do not store ownership information.

@@ -53,12 +163,17 @@ func processFileInZipArchive(file *zip.File, tarWriter *tar.Writer, maxSize int6

53163

return err

54164

}

5516556-

n, err := io.CopyN(tarWriter, fileReader, maxSize)

57-

log.Println(file.Name, n, err)

58-

if errors.Is(err, io.EOF) {

59-

err = nil

166+

_, err = io.CopyN(tarWriter, fileReader, size)

167+

switch {

168+

case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):

169+

return ErrInvalidZipContent

170+

case errors.Is(err, zip.ErrChecksum), errors.Is(err, zip.ErrFormat):

171+

return ErrInvalidZipContent

172+

case err != nil:

173+

return err

174+

default:

175+

return nil

60176

}

61-

return err

62177

}

6317864179

// CreateZipFromTar converts the given tarReader to a zip archive.