mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-04 01:38:12 +08:00
Merge 1658e4e41033b35079aec505b038e0d3993b89ad into d3ffc9985281dcf4d3bef604cce4e662b1a327a6
This commit is contained in:
commit
fbd5c36272
60
context.go
60
context.go
@ -717,6 +717,46 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
|
|||||||
|
|
||||||
// SaveUploadedFile uploads the form file to specific dst.
|
// SaveUploadedFile uploads the form file to specific dst.
|
||||||
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
|
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
|
||||||
|
return c.saveUploadedFile(file, dst, perm,
|
||||||
|
os.MkdirAll,
|
||||||
|
os.Chmod,
|
||||||
|
func(name string) (*os.File, error) {
|
||||||
|
return os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUploadedFileToRoot uploads the form file to dst within root.
|
||||||
|
//
|
||||||
|
// Unlike SaveUploadedFile, all filesystem operations are constrained to root,
|
||||||
|
// so path traversal and symlink escapes outside root are rejected by os.Root.
|
||||||
|
// This method requires Go 1.25+.
|
||||||
|
func (c *Context) SaveUploadedFileToRoot(file *multipart.FileHeader, dst string, root *os.Root, perm ...fs.FileMode) error {
|
||||||
|
if root == nil {
|
||||||
|
return errors.New("root is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.saveUploadedFile(file, dst, perm,
|
||||||
|
func(name string, mode os.FileMode) error {
|
||||||
|
return root.MkdirAll(name, mode)
|
||||||
|
},
|
||||||
|
func(name string, mode os.FileMode) error {
|
||||||
|
return root.Chmod(name, mode)
|
||||||
|
},
|
||||||
|
func(name string) (*os.File, error) {
|
||||||
|
return root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) saveUploadedFile(
|
||||||
|
file *multipart.FileHeader,
|
||||||
|
dst string,
|
||||||
|
perm []fs.FileMode,
|
||||||
|
mkdirAll func(string, os.FileMode) error,
|
||||||
|
chmod func(string, os.FileMode) error,
|
||||||
|
openFile func(string) (*os.File, error),
|
||||||
|
) error {
|
||||||
src, err := file.Open()
|
src, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -725,17 +765,21 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
|
|||||||
|
|
||||||
var mode os.FileMode = 0o750
|
var mode os.FileMode = 0o750
|
||||||
if len(perm) > 0 {
|
if len(perm) > 0 {
|
||||||
mode = perm[0]
|
mode = os.FileMode(perm[0])
|
||||||
}
|
}
|
||||||
|
dst = filepath.Clean(dst)
|
||||||
dir := filepath.Dir(dst)
|
dir := filepath.Dir(dst)
|
||||||
if err = os.MkdirAll(dir, mode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = os.Chmod(dir, mode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := os.Create(dst)
|
if dir != "." && dir != "/" {
|
||||||
|
if err = mkdirAll(dir, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = chmod(dir, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, err := openFile(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
104
context_test.go
104
context_test.go
@ -274,6 +274,110 @@ func TestSaveUploadedFileWithPermissionFailed(t *testing.T) {
|
|||||||
require.Error(t, c.SaveUploadedFile(f, "test/permission_test", mode))
|
require.Error(t, c.SaveUploadedFile(f, "test/permission_test", mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSaveUploadedFileToRoot(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)
|
||||||
|
|
||||||
|
rootDir := t.TempDir()
|
||||||
|
root, err := os.OpenRoot(rootDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, root.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
var mode fs.FileMode = 0o755
|
||||||
|
require.NoError(t, c.SaveUploadedFileToRoot(f, "test/permission_test", root, mode))
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filepath.Join(rootDir, "test", "permission_test"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "permission_test", string(content))
|
||||||
|
|
||||||
|
info, err := os.Stat(filepath.Join(rootDir, "test"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, mode, info.Mode().Perm())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveUploadedFileToRootRejectsPathTraversal(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(buf)
|
||||||
|
w, err := mw.CreateFormFile("file", "escape.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = w.Write([]byte("escape"))
|
||||||
|
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)
|
||||||
|
|
||||||
|
baseDir := t.TempDir()
|
||||||
|
rootDir := filepath.Join(baseDir, "root")
|
||||||
|
require.NoError(t, os.Mkdir(rootDir, 0o755))
|
||||||
|
root, err := os.OpenRoot(rootDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, root.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
err = c.SaveUploadedFileToRoot(f, "../escape.txt", root)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorContains(t, err, "path escapes")
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(baseDir, "escape.txt"))
|
||||||
|
require.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveUploadedFileToRootRejectsSymlinkEscape(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
mw := multipart.NewWriter(buf)
|
||||||
|
w, err := mw.CreateFormFile("file", "escape.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = w.Write([]byte("escape"))
|
||||||
|
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)
|
||||||
|
|
||||||
|
baseDir := t.TempDir()
|
||||||
|
rootDir := filepath.Join(baseDir, "root")
|
||||||
|
outsideDir := filepath.Join(baseDir, "outside")
|
||||||
|
require.NoError(t, os.Mkdir(rootDir, 0o755))
|
||||||
|
require.NoError(t, os.Mkdir(outsideDir, 0o755))
|
||||||
|
|
||||||
|
if err := os.Symlink(outsideDir, filepath.Join(rootDir, "link")); err != nil {
|
||||||
|
t.Skipf("symlink unsupported: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := os.OpenRoot(rootDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, root.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
err = c.SaveUploadedFileToRoot(f, "link/escape.txt", root)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(outsideDir, "escape.txt"))
|
||||||
|
require.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextReset(t *testing.T) {
|
func TestContextReset(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
c := router.allocateContext(0)
|
c := router.allocateContext(0)
|
||||||
|
|||||||
17
docs/doc.md
17
docs/doc.md
@ -281,6 +281,23 @@ References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail
|
|||||||
|
|
||||||
> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.
|
> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.
|
||||||
|
|
||||||
|
If `dst` comes from user input, prefer constraining writes with `os.OpenRoot` and `SaveUploadedFileToRoot` so `..` traversal and symlink escapes cannot write outside your upload directory.
|
||||||
|
|
||||||
|
`os.Root` is available in Go 1.25+.
|
||||||
|
|
||||||
|
```go
|
||||||
|
root, err := os.OpenRoot("./uploads")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer root.Close()
|
||||||
|
|
||||||
|
if err := c.SaveUploadedFileToRoot(file, dst, root); err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "upload failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user