From 1654182f8d455ded2dcc13c7052cd706258c9fe6 Mon Sep 17 00:00:00 2001 From: tengtian Date: Wed, 15 Apr 2026 20:02:50 +0200 Subject: [PATCH 1/6] fix: skip chmod on existing directories in SaveUploadedFile SaveUploadedFile called os.Chmod on the target directory even if it already existed. This breaks when saving to system directories like /tmp where the process lacks permission to chmod. Now only newly created directories get chmod'd. Existing directories are left as-is, which is the correct behavior since the caller should not modify permissions of directories they don't own. Fixes #4622 --- context.go | 14 ++++++++++++-- context_test.go | 17 ++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/context.go b/context.go index a2e28e5b..3499fc2c 100644 --- a/context.go +++ b/context.go @@ -715,6 +715,11 @@ func (c *Context) MultipartForm() (*multipart.Form, error) { return c.Request.MultipartForm, err } +func dirExists(path string) bool { + info, err := os.Stat(path) + return err == nil && info.IsDir() +} + // SaveUploadedFile uploads the form file to specific dst. func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error { src, err := file.Open() @@ -728,11 +733,16 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm mode = perm[0] } dir := filepath.Dir(dst) + dirExisted := dirExists(dir) if err = os.MkdirAll(dir, mode); err != nil { return err } - if err = os.Chmod(dir, mode); err != nil { - return err + // Only chmod newly created directories. Attempting to chmod + // pre-existing directories (e.g. /tmp) may fail with EPERM. + if !dirExisted { + if err = os.Chmod(dir, mode); err != nil { + return err + } } out, err := os.Create(dst) diff --git a/context_test.go b/context_test.go index 364a92ae..6b5fc945 100644 --- a/context_test.go +++ b/context_test.go @@ -21,6 +21,7 @@ import ( "os" "path/filepath" "reflect" + "runtime" "strconv" "strings" "sync" @@ -248,13 +249,15 @@ func TestSaveUploadedFileWithPermission(t *testing.T) { require.NoError(t, err) assert.Equal(t, "permission_test", f.Filename) var mode fs.FileMode = 0o755 - require.NoError(t, c.SaveUploadedFile(f, "permission_test", mode)) - t.Cleanup(func() { - assert.NoError(t, os.Remove("permission_test")) - }) - info, err := os.Stat(filepath.Dir("permission_test")) - require.NoError(t, err) - assert.Equal(t, info.Mode().Perm(), mode) + tmpDir := t.TempDir() + newSubDir := filepath.Join(tmpDir, "newdir") + dst := filepath.Join(newSubDir, "permission_test") + require.NoError(t, c.SaveUploadedFile(f, dst, mode)) + if runtime.GOOS != "windows" { + info, err := os.Stat(newSubDir) + require.NoError(t, err) + assert.Equal(t, mode, info.Mode().Perm()) + } } func TestSaveUploadedFileWithPermissionFailed(t *testing.T) { From 1baaa8ba8e6f750434c4af1daa749e896d16d602 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Fri, 22 May 2026 13:02:47 +0200 Subject: [PATCH 2/6] test: cover uploaded file directory permissions --- context_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/context_test.go b/context_test.go index 6b5fc945..f523525d 100644 --- a/context_test.go +++ b/context_test.go @@ -260,6 +260,51 @@ func TestSaveUploadedFileWithPermission(t *testing.T) { } } +func TestDirExists(t *testing.T) { + tmpDir := t.TempDir() + assert.True(t, dirExists(tmpDir)) + assert.False(t, dirExists(filepath.Join(tmpDir, "missing"))) + + filePath := filepath.Join(tmpDir, "file") + require.NoError(t, os.WriteFile(filePath, []byte("test"), 0o600)) + assert.False(t, dirExists(filePath)) +} + +func TestSaveUploadedFileKeepsExistingDirPermission(t *testing.T) { + buf := new(bytes.Buffer) + mw := multipart.NewWriter(buf) + w, err := mw.CreateFormFile("file", "permission_test") + require.NoError(t, err) + _, err = w.Write([]byte("permission_test")) + require.NoError(t, err) + mw.Close() + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest(http.MethodPost, "/", buf) + c.Request.Header.Set("Content-Type", mw.FormDataContentType()) + f, err := c.FormFile("file") + require.NoError(t, err) + + tmpDir := t.TempDir() + existingDir := filepath.Join(tmpDir, "existing") + require.NoError(t, os.Mkdir(existingDir, 0o700)) + if runtime.GOOS != "windows" { + require.NoError(t, os.Chmod(existingDir, 0o700)) + } + + var mode fs.FileMode = 0o755 + dst := filepath.Join(existingDir, "permission_test") + require.NoError(t, c.SaveUploadedFile(f, dst, mode)) + + saved, err := os.ReadFile(dst) + require.NoError(t, err) + assert.Equal(t, "permission_test", string(saved)) + if runtime.GOOS != "windows" { + info, err := os.Stat(existingDir) + require.NoError(t, err) + assert.Equal(t, fs.FileMode(0o700), info.Mode().Perm()) + } +} + func TestSaveUploadedFileWithPermissionFailed(t *testing.T) { buf := new(bytes.Buffer) mw := multipart.NewWriter(buf) From 0538db25909d039b1bbf0a417bbf98f31348d518 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Fri, 22 May 2026 13:09:13 +0200 Subject: [PATCH 3/6] test: improve upload permission coverage --- context.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 3499fc2c..ee1dc7c6 100644 --- a/context.go +++ b/context.go @@ -740,9 +740,10 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm // Only chmod newly created directories. Attempting to chmod // pre-existing directories (e.g. /tmp) may fail with EPERM. if !dirExisted { - if err = os.Chmod(dir, mode); err != nil { - return err - } + err = os.Chmod(dir, mode) + } + if err != nil { + return err } out, err := os.Create(dst) From 880ef9d904063e8a3ae826a6fc6f25bd5861504a Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Fri, 22 May 2026 13:29:52 +0200 Subject: [PATCH 4/6] test: avoid partial coverage in dir helper --- context.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index ee1dc7c6..4258f974 100644 --- a/context.go +++ b/context.go @@ -717,7 +717,10 @@ func (c *Context) MultipartForm() (*multipart.Form, error) { func dirExists(path string) bool { info, err := os.Stat(path) - return err == nil && info.IsDir() + if err != nil { + return false + } + return info.IsDir() } // SaveUploadedFile uploads the form file to specific dst. From 39ac842d6246311ca4102d9c602ca605b3b5008e Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Fri, 22 May 2026 13:35:04 +0200 Subject: [PATCH 5/6] refactor: cover directory setup helper --- context.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 4258f974..911753b8 100644 --- a/context.go +++ b/context.go @@ -723,6 +723,16 @@ func dirExists(path string) bool { return info.IsDir() } +func ensureDirWithMode(dir string, mode fs.FileMode) error { + if dirExists(dir) { + return nil + } + if err := os.MkdirAll(dir, mode); err != nil { + return err + } + return os.Chmod(dir, mode) +} + // SaveUploadedFile uploads the form file to specific dst. func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error { src, err := file.Open() @@ -736,16 +746,9 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm mode = perm[0] } dir := filepath.Dir(dst) - dirExisted := dirExists(dir) - if err = os.MkdirAll(dir, mode); err != nil { - return err - } // Only chmod newly created directories. Attempting to chmod // pre-existing directories (e.g. /tmp) may fail with EPERM. - if !dirExisted { - err = os.Chmod(dir, mode) - } - if err != nil { + if err = ensureDirWithMode(dir, mode); err != nil { return err } From dfff59f1fa93b76a4009741b7500ad1341f7c466 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:16:40 +0200 Subject: [PATCH 6/6] chore: bump quic-go for trivy scan --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index df181253..c67c5fed 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/modern-go/reflect2 v1.0.2 github.com/pelletier/go-toml/v2 v2.2.4 - github.com/quic-go/quic-go v0.59.0 + github.com/quic-go/quic-go v0.59.1 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 go.mongodb.org/mongo-driver/v2 v2.5.0 diff --git a/go.sum b/go.sum index f7f9e27b..e69bfeee 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=