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.
1433func CreateTarFromZip(zipReader *zip.Reader, maxSize int64) ([]byte, error) {
34+err := validateZipArchiveSize(zipReader, maxSize)
35+if err != nil {
36+return nil, err
37+ }
38+1539var tarBuffer bytes.Buffer
16-err := writeTarArchive(&tarBuffer, zipReader, maxSize)
40+err = writeTarArchive(&tarBuffer, zipReader, maxSize)
1741if err != nil {
1842return nil, err
1943 }
2044return 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+ }
26602761for _, file := range zipReader.File {
28-err := processFileInZipArchive(file, tarWriter, maxSize)
62+entrySize, err := projectedTarEntrySize(file)
2963if err != nil {
3064return err
3165 }
66+if entrySize > maxBytes-totalBytes {
67+return ErrArchiveTooLarge
68+ }
69+totalBytes += entrySize
3270 }
71+3372return 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 {
37142fileReader, err := file.Open()
38143if err != nil {
39144return err
40145 }
41146defer fileReader.Close()
42147148+size := file.FileInfo().Size()
149+if size < 0 {
150+return ErrArchiveTooLarge
151+ }
152+43153err = tarWriter.WriteHeader(&tar.Header{
44154Name: file.Name,
45-Size: file.FileInfo().Size(),
155+Size: size,
46156Mode: int64(file.Mode()),
47157ModTime: 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
53163return 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.