diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d7cc4afd..37d1adb66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v5.0.3 - 2026-02-06 + +**Security** + +* Fix directory traversal vulnerability under Windows in Static middleware when default Echo filesystem is used. Reported by @shblue21. + +This applies to cases when: +- Windows is used as OS +- `middleware.StaticConfig.Filesystem` is `nil` (default) +- `echo.Filesystem` is has not been set explicitly (default) + +Exposure is restricted to the active process working directory and its subfolders. + + ## v5.0.2 - 2026-02-02 **Security** diff --git a/_fixture/dist/public/test.txt b/_fixture/dist/public/test.txt new file mode 100644 index 000000000..dd937160d --- /dev/null +++ b/_fixture/dist/public/test.txt @@ -0,0 +1 @@ +test.txt contents diff --git a/context.go b/context.go index 3511cf7ac..f91ea7a60 100644 --- a/context.go +++ b/context.go @@ -15,6 +15,7 @@ import ( "net" "net/http" "net/url" + "path" "path/filepath" "strings" "sync" @@ -579,6 +580,7 @@ func (c *Context) FileFS(file string, filesystem fs.FS) error { } func fsFile(c *Context, file string, filesystem fs.FS) error { + file = path.Clean(file) // `os.Open` and `os.DirFs.Open()` behave differently, later does not like ``, `.`, `..` at all, but we allowed those now need to clean f, err := filesystem.Open(file) if err != nil { return ErrNotFound diff --git a/echo.go b/echo.go index 4e3899516..4855e8429 100644 --- a/echo.go +++ b/echo.go @@ -785,14 +785,11 @@ func newDefaultFS() *defaultFS { dir, _ := os.Getwd() return &defaultFS{ prefix: dir, - fs: nil, + fs: os.DirFS(dir), } } func (fs defaultFS) Open(name string) (fs.File, error) { - if fs.fs == nil { - return os.Open(name) // #nosec G304 - } return fs.fs.Open(name) } diff --git a/group_test.go b/group_test.go index 819b6df97..7078b6497 100644 --- a/group_test.go +++ b/group_test.go @@ -467,13 +467,14 @@ func TestGroup_Static(t *testing.T) { func TestGroup_StaticMultiTest(t *testing.T) { var testCases = []struct { - name string - givenPrefix string - givenRoot string - whenURL string - expectHeaderLocation string - expectBodyStartsWith string - expectStatus int + name string + givenPrefix string + givenRoot string + whenURL string + expectHeaderLocation string + expectBodyStartsWith string + expectBodyNotContains string + expectStatus int }{ { name: "ok", @@ -582,6 +583,22 @@ func TestGroup_StaticMultiTest(t *testing.T) { expectStatus: http.StatusOK, expectBodyStartsWith: "", }, + { + name: "nok, URL encoded path traversal (single encoding, slash - unix separator)", + givenRoot: "_fixture/dist/public", + whenURL: "/%2e%2e%2fprivate.txt", + expectStatus: http.StatusNotFound, + expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", + expectBodyNotContains: `private file`, + }, + { + name: "nok, URL encoded path traversal (single encoding, backslash - windows separator)", + givenRoot: "_fixture/dist/public", + whenURL: "/%2e%2e%5cprivate.txt", + expectStatus: http.StatusNotFound, + expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", + expectBodyNotContains: `private file`, + }, { name: "do not allow directory traversal (backslash - windows separator)", givenPrefix: "/", @@ -618,6 +635,9 @@ func TestGroup_StaticMultiTest(t *testing.T) { } else { assert.Equal(t, "", body) } + if tc.expectBodyNotContains != "" { + assert.NotContains(t, body, tc.expectBodyNotContains) + } if tc.expectHeaderLocation != "" { assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0]) diff --git a/middleware/static.go b/middleware/static.go index ee1c8bee9..452603325 100644 --- a/middleware/static.go +++ b/middleware/static.go @@ -15,6 +15,7 @@ import ( "path" "strconv" "strings" + "sync" "github.com/labstack/echo/v5" ) @@ -118,13 +119,12 @@ const directoryListHTMLTemplate = `