mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
2f011fd2a3
Enforce aggregate limits when converting uploaded ZIP archives to tar so compressed inputs cannot expand without bound in memory. Also treat malformed ZIP entry metadata and content mismatches as client errors during conversion, returning 400 for invalid archives and 413 when expanded tar output exceeds the upload limit. Ref: https://linear.app/codercom/issue/PLAT-274/zip-upload-decompressed-without-aggregate-size-limit-sec-103
262 lines
7.5 KiB
Go
262 lines
7.5 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/archive"
|
|
"github.com/coder/coder/v2/archive/archivetest"
|
|
"github.com/coder/coder/v2/coderd"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestPostFiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
buildZipWithFile := func(t *testing.T, name string, writeContents func(w io.Writer) error) []byte {
|
|
t.Helper()
|
|
|
|
var zipBytes bytes.Buffer
|
|
zw := zip.NewWriter(&zipBytes)
|
|
w, err := zw.Create(name)
|
|
require.NoError(t, err)
|
|
require.NoError(t, writeContents(w))
|
|
require.NoError(t, zw.Close())
|
|
|
|
return zipBytes.Bytes()
|
|
}
|
|
|
|
// Single instance shared across all sub-tests. Each sub-test
|
|
// creates independent resources with unique IDs so parallel
|
|
// execution is safe.
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
t.Run("BadContentType", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.Upload(ctx, "bad", bytes.NewReader([]byte{'a'}))
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("Insert", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(make([]byte, 1024)))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("InsertWindowsZip", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.Upload(ctx, "application/x-zip-compressed", bytes.NewReader(archivetest.TestZipFileBytes()))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("InsertAlreadyExists", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
data := make([]byte, 1024)
|
|
_, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(data))
|
|
require.NoError(t, err)
|
|
_, err = client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(data))
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("InvalidZipMetadata", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
corruptZipUncompressedSize := func(t *testing.T, zipBytes []byte, size uint32) []byte {
|
|
t.Helper()
|
|
|
|
const (
|
|
directoryHeaderSignature = "PK\x01\x02"
|
|
uncompressedSizeOffset = 24
|
|
)
|
|
hdrOffset := bytes.Index(zipBytes, []byte(directoryHeaderSignature))
|
|
require.NotEqual(t, -1, hdrOffset, "missing ZIP central directory header")
|
|
corrupted := bytes.Clone(zipBytes)
|
|
sizeBytes := corrupted[hdrOffset+uncompressedSizeOffset : hdrOffset+uncompressedSizeOffset+4]
|
|
binary.LittleEndian.PutUint32(sizeBytes, size)
|
|
|
|
return corrupted
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
zipBytes := buildZipWithFile(t, "hello.txt", func(w io.Writer) error {
|
|
_, err := w.Write([]byte("hello"))
|
|
return err
|
|
})
|
|
zipBytes = corruptZipUncompressedSize(t, zipBytes, 6)
|
|
|
|
_, err := client.Upload(ctx, codersdk.ContentTypeZip, bytes.NewReader(zipBytes))
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
|
})
|
|
t.Run("InsertConcurrent", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
var wg sync.WaitGroup
|
|
var end sync.WaitGroup
|
|
wg.Add(1)
|
|
end.Add(3)
|
|
for range 3 {
|
|
go func() {
|
|
wg.Wait()
|
|
data := make([]byte, 1024)
|
|
_, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(data))
|
|
end.Done()
|
|
assert.NoError(t, err)
|
|
}()
|
|
}
|
|
wg.Done()
|
|
end.Wait()
|
|
})
|
|
//nolint:paralleltest // This subtest is intentionally serial to
|
|
// avoid extra memory pressure.
|
|
t.Run("OversizedZipExpansion", func(t *testing.T) {
|
|
buildZipWithSizedFile := func(t *testing.T, name string, size int64) []byte {
|
|
return buildZipWithFile(t, name, func(w io.Writer) error {
|
|
chunk := bytes.Repeat([]byte("a"), 32*1024)
|
|
for written := int64(0); written < size; {
|
|
n := len(chunk)
|
|
if remaining := size - written; int64(n) > remaining {
|
|
n = int(remaining)
|
|
}
|
|
|
|
_, err := w.Write(chunk[:n])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
written += int64(n)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Leave only enough room for the tar trailer. The single
|
|
// entry header then pushes the converted tar output over the
|
|
// file size limit.
|
|
size := int64(coderd.HTTPFileMaxBytes - 1024)
|
|
zipBytes := buildZipWithSizedFile(t, "oversized.txt", size)
|
|
|
|
_, err := client.Upload(ctx, codersdk.ContentTypeZip, bytes.NewReader(zipBytes))
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusRequestEntityTooLarge, apiErr.StatusCode())
|
|
})
|
|
}
|
|
|
|
func TestDownload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Shared instance — see TestPostFiles for rationale.
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, _, err := client.Download(ctx, uuid.New())
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
|
})
|
|
|
|
t.Run("InsertTar_DownloadTar", func(t *testing.T) {
|
|
t.Parallel()
|
|
// given
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
tarball := archivetest.TestTarFileBytes()
|
|
|
|
// when
|
|
resp, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(tarball))
|
|
require.NoError(t, err)
|
|
data, contentType, err := client.Download(ctx, resp.ID)
|
|
require.NoError(t, err)
|
|
|
|
// then
|
|
require.Len(t, data, len(tarball))
|
|
require.Equal(t, codersdk.ContentTypeTar, contentType)
|
|
require.Equal(t, tarball, data)
|
|
archivetest.AssertSampleTarFile(t, data)
|
|
})
|
|
|
|
t.Run("InsertZip_DownloadTar", func(t *testing.T) {
|
|
t.Parallel()
|
|
// given
|
|
zipContent := archivetest.TestZipFileBytes()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// when
|
|
resp, err := client.Upload(ctx, codersdk.ContentTypeZip, bytes.NewReader(zipContent))
|
|
require.NoError(t, err)
|
|
data, contentType, err := client.Download(ctx, resp.ID)
|
|
require.NoError(t, err)
|
|
|
|
// then
|
|
require.Equal(t, codersdk.ContentTypeTar, contentType)
|
|
|
|
// Note: creating a zip from a tar will result in some loss of information
|
|
// as zip files do not store UNIX user:group data.
|
|
archivetest.AssertSampleTarFile(t, data)
|
|
})
|
|
|
|
t.Run("InsertTar_DownloadZip", func(t *testing.T) {
|
|
t.Parallel()
|
|
// given
|
|
tarball := archivetest.TestTarFileBytes()
|
|
|
|
tarReader := tar.NewReader(bytes.NewReader(tarball))
|
|
expectedZip, err := archive.CreateZipFromTar(tarReader, 10240)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// when
|
|
resp, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(tarball))
|
|
require.NoError(t, err)
|
|
data, contentType, err := client.DownloadWithFormat(ctx, resp.ID, codersdk.FormatZip)
|
|
require.NoError(t, err)
|
|
|
|
// then
|
|
require.Equal(t, codersdk.ContentTypeZip, contentType)
|
|
require.Equal(t, expectedZip, data)
|
|
archivetest.AssertSampleZipFile(t, data)
|
|
})
|
|
}
|