Echo has a Windows path traversal via backslash in middleware.Static default filesystem
Description
Echo is a Go web framework. In versions 5.0.0 through 5.0.2 on Windows, Echo’s middleware.Static using the default filesystem allows path traversal via backslashes, enabling unauthenticated remote file read outside the static root. In middleware/static.go, the requested path is unescaped and normalized with path.Clean (URL semantics). path.Clean does not treat \ as a path separator, so ..\ sequences remain in the cleaned path. The resulting path is then passed to currentFS.Open(...). When the filesystem is left at the default (nil), Echo uses defaultFS which calls os.Open (echo.go:792). On Windows, os.Open treats \ as a path separator and resolves ..\, allowing traversal outside the static root. Version 5.0.3 fixes the issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Echo static middleware on Windows allows path traversal via backslashes, enabling unauthenticated file read; fixed in v5.0.3.
Vulnerability
CVE-2026-25766 is a path traversal vulnerability in the Echo web framework (Go) affecting versions 5.0.0 through 5.0.2 on Windows. When the Static middleware is used with the default filesystem (nil), crafted requests containing backslashes (..\) bypass path sanitization. In middleware/static.go, the requested path is cleaned using path.Clean, which does not treat backslashes as path separators on Windows, leaving ..\ sequences intact. The resulting path is then passed to os.Open, which does interpret backslashes, allowing traversal outside the static root [1][2].
Exploitation
An unauthenticated remote attacker can send a specially crafted HTTP request to the static file handler with a path containing ..\ sequences to read arbitrary files on the Windows filesystem. No authentication is required, and the attack is trivial to execute if the static middleware is exposed [1]. The vulnerability was introduced in the first v5 proposal branch around October 2021 and remained undetected because test cases used a custom filesystem rather than the default [4].
Impact
Successful exploitation allows an attacker to read sensitive files outside the intended static root directory, such as configuration files, application source code, or other system files. This could lead to information disclosure and further compromise of the application or server.
Mitigation
The issue is fixed in Echo v5.0.3. Users should upgrade immediately. The fix, implemented in commit b1d443086ea27cf51345ec72a71e9b7e9d9ce5f1, includes sanitizing the Root configuration and handling the default filesystem to prevent traversal [2]. If upgrading is not possible, users can use a custom filesystem that properly sanitizes paths [1][4].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/labstack/echo/v5Go | >= 5.0.0, < 5.0.3 | 5.0.3 |
Affected products
2- labstack/echov5Range: >= 5.0.0, < 5.0.3
Patches
1b1d443086ea2Merge pull request #2891 from aldas/fix_staticmw
16 files changed · +195 −286
CHANGELOG.md+14 −0 modified@@ -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**
context.go+2 −0 modified@@ -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
echo.go+1 −4 modified@@ -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) }
_fixture/dist/public/test.txt+1 −0 added@@ -0,0 +1 @@ +test.txt contents
group_test.go+27 −7 modified@@ -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: "<!doctype html>", }, + { + 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])
middleware/static.go+35 −12 modified@@ -15,6 +15,7 @@ import ( "path" "strconv" "strings" + "sync" "github.com/labstack/echo/v5" ) @@ -118,13 +119,12 @@ const directoryListHTMLTemplate = ` </header> <ul> {{ range .Files }} - {{ $href := .Name }}{{ if ne $.Name "/" }}{{ $href = print $.Name "/" .Name }}{{ end }} <li> {{ if .Dir }} {{ $name := print .Name "/" }} - <a class="dir" href="{{ $href }}">{{ $name }}</a> + <a class="dir" href="{{ $name }}">{{ $name }}</a> {{ else }} - <a class="file" href="{{ $href }}">{{ .Name }}</a> + <a class="file" href="{{ .Name }}">{{ .Name }}</a> <span>{{ .Size }}</span> {{ end }} </li> @@ -157,7 +157,10 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) { // Defaults if config.Root == "" { config.Root = "." // For security we want to restrict to CWD. + } else { + config.Root = path.Clean(config.Root) // fs.Open is very picky about ``, `.`, `..` in paths, so remove some of them up. } + if config.Skipper == nil { config.Skipper = DefaultStaticConfig.Skipper } @@ -173,6 +176,19 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) { return nil, fmt.Errorf("echo static middleware directory list template parsing error: %w", tErr) } + var once *sync.Once + var fsErr error + currentFS := config.Filesystem + if config.Filesystem == nil { + once = &sync.Once{} + } else if config.Root != "." { + tmpFs, fErr := fs.Sub(config.Filesystem, path.Join(".", config.Root)) + if fErr != nil { + return nil, fmt.Errorf("static middleware failed to create sub-filesystem from config.Root, error: %w", fErr) + } + currentFS = tmpFs + } + return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) (err error) { if config.Skipper(c) { @@ -197,8 +213,7 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) { // 3. The "/" prefix forces absolute path interpretation, removing ".." components // 4. Backslashes are treated as literal characters (not path separators), preventing traversal // See static_windows.go for Go 1.20+ filepath.Clean compatibility notes - requestedPath := path.Clean("/" + p) // "/"+ for security - filePath := path.Join(config.Root, requestedPath) + filePath := path.Clean("./" + p) if config.IgnoreBase { routePath := path.Base(strings.TrimRight(c.Path(), "/*")) @@ -209,9 +224,17 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) { } } - currentFS := config.Filesystem - if currentFS == nil { - currentFS = c.Echo().Filesystem + if once != nil { + once.Do(func() { + if tmp, tmpErr := fs.Sub(c.Echo().Filesystem, config.Root); tmpErr != nil { + fsErr = fmt.Errorf("static middleware failed to create sub-filesystem: %w", tmpErr) + } else { + currentFS = tmp + } + }) + if fsErr != nil { + return fsErr + } } file, err := currentFS.Open(filePath) @@ -231,7 +254,7 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) { return err } // is case HTML5 mode is enabled + echo 404 we serve index to the client - file, err = currentFS.Open(path.Join(config.Root, config.Index)) + file, err = currentFS.Open(config.Index) if err != nil { return err } @@ -248,7 +271,7 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) { index, err := currentFS.Open(path.Join(filePath, config.Index)) if err != nil { if config.Browse { - return listDir(dirListTemplate, requestedPath, filePath, currentFS, c.Response()) + return listDir(dirListTemplate, filePath, currentFS, c.Response()) } return next(c) @@ -278,7 +301,7 @@ func serveFile(c *echo.Context, file fs.File, info os.FileInfo) error { return nil } -func listDir(t *template.Template, requestedPath string, pathInFs string, filesystem fs.FS, res http.ResponseWriter) error { +func listDir(t *template.Template, pathInFs string, filesystem fs.FS, res http.ResponseWriter) error { files, err := fs.ReadDir(filesystem, pathInFs) if err != nil { return fmt.Errorf("static middleware failed to read directory for listing: %w", err) @@ -290,7 +313,7 @@ func listDir(t *template.Template, requestedPath string, pathInFs string, filesy Name string Files []any }{ - Name: requestedPath, + Name: pathInFs, } for _, f := range files {
middleware/static_other.go+18 −3 modified@@ -1,15 +1,30 @@ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors -//go:build !windows - package middleware import ( + "errors" + "io/fs" "os" ) // We ignore these errors as there could be handler that matches request path. func isIgnorableOpenFileError(err error) bool { - return os.IsNotExist(err) + if os.IsNotExist(err) { + return true + } + // As of Go 1.20 Windows path checks are more strict on the provided path and considers [UNC](https://en.wikipedia.org/wiki/Path_(computing)#UNC) + // paths with missing host etc parts as invalid. Previously it would result you `fs.ErrNotExist`. + // Also `fs.Open` on all OSes does not accept ``, `.`, `..` at all. + // + // so we need to treat those errors the same as `fs.ErrNotExists` so we can continue handling + // errors in the middleware/handler chain. Otherwise we might end up with status 500 instead of finding a route + // or return 404 not found. + var pErr *fs.PathError + if errors.As(err, &pErr) { + err = pErr.Err + return err.Error() == "invalid argument" + } + return false }
middleware/static_test.go+90 −225 modified@@ -8,7 +8,6 @@ import ( "net/http" "net/http/httptest" "os" - "strings" "testing" "testing/fstest" @@ -21,9 +20,7 @@ func TestStatic_useCaseForApiAndSPAs(t *testing.T) { // serve single page application (SPA) files from server root e.Use(StaticWithConfig(StaticConfig{ - Root: ".", - // by default Echo filesystem is fixed to `./` but this does not allow `../` (moving up in folder structure past filesystem root) - Filesystem: os.DirFS("../_fixture"), + Root: "testdata/dist/public", })) // all requests to `/api/*` will end up in echo handlers (assuming there is not `api` folder and files) @@ -43,7 +40,7 @@ func TestStatic_useCaseForApiAndSPAs(t *testing.T) { rec = httptest.NewRecorder() e.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) - assert.Contains(t, rec.Body.String(), "<title>Echo</title>") + assert.Contains(t, rec.Body.String(), "<h1>Hello from index</h1>\n") } @@ -54,62 +51,63 @@ func TestStatic(t *testing.T) { givenAttachedToGroup string whenURL string expectContains string + expectNotContains string expectLength string expectCode int }{ { name: "ok, serve index with Echo message", whenURL: "/", expectCode: http.StatusOK, - expectContains: "<title>Echo</title>", + expectContains: "<h1>Hello from index</h1>", }, { - name: "ok, serve file from subdirectory", - whenURL: "/images/walle.png", - expectCode: http.StatusOK, - expectLength: "219885", + name: "ok, serve file from subdirectory", + whenURL: "/assets/readme.md", + expectCode: http.StatusOK, + expectContains: "This directory is used for the static middleware test", }, { name: "ok, when html5 mode serve index for any static file that does not exist", givenConfig: &StaticConfig{ - Root: "_fixture", + Root: "testdata/dist/public", HTML5: true, }, whenURL: "/random", expectCode: http.StatusOK, - expectContains: "<title>Echo</title>", + expectContains: "<h1>Hello from index</h1>", }, { name: "ok, serve index as directory index listing files directory", givenConfig: &StaticConfig{ - Root: "_fixture/certs", + Root: "testdata/dist/public/assets", Browse: true, }, whenURL: "/", expectCode: http.StatusOK, - expectContains: "cert.pem", + expectContains: `<a class="file" href="readme.md">readme.md</a>`, }, { name: "ok, serve directory index with IgnoreBase and browse", givenConfig: &StaticConfig{ - Root: "_fixture/_fixture/", // <-- last `_fixture/` is overlapping with group path and needs to be ignored + Root: "testdata/dist/public/assets/", // <-- last `assets/` is overlapping with group path and needs to be ignored IgnoreBase: true, Browse: true, }, - givenAttachedToGroup: "/_fixture", - whenURL: "/_fixture/", + givenAttachedToGroup: "/assets", + whenURL: "/assets/", expectCode: http.StatusOK, - expectContains: `<a class="file" href="README.md">README.md</a>`, + expectContains: `<a class="file" href="readme.md">readme.md</a>`, }, { name: "ok, serve file with IgnoreBase", givenConfig: &StaticConfig{ - Root: "_fixture/_fixture/", // <-- last `_fixture/` is overlapping with group path and needs to be ignored + Root: "testdata/dist/public/assets", // <-- last `assets/` is overlapping with group path and needs to be ignored IgnoreBase: true, Browse: true, }, - givenAttachedToGroup: "/_fixture", - whenURL: "/_fixture/README.md", + givenAttachedToGroup: "/assets", + whenURL: "/assets/readme.md", expectCode: http.StatusOK, expectContains: "This directory is used for the static middleware test", }, @@ -119,18 +117,6 @@ func TestStatic(t *testing.T) { expectCode: http.StatusNotFound, expectContains: "{\"message\":\"Not Found\"}\n", }, - { - name: "nok, do not allow directory traversal (backslash - windows separator)", - whenURL: `/..\\middleware/basic_auth.go`, - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", - }, - { - name: "nok,do not allow directory traversal (slash - unix separator)", - whenURL: `/../middleware/basic_auth.go`, - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", - }, { name: "ok, when no file then a handler will care of the request", whenURL: "/regular-handler", @@ -140,7 +126,7 @@ func TestStatic(t *testing.T) { { name: "ok, skip middleware and serve handler", givenConfig: &StaticConfig{ - Root: "_fixture/images/", + Root: "testdata/dist/public", Skipper: func(c *echo.Context) bool { return true }, @@ -152,46 +138,78 @@ func TestStatic(t *testing.T) { { name: "nok, when html5 fail if the index file does not exist", givenConfig: &StaticConfig{ - Root: "_fixture", + Root: "testdata/dist/public", HTML5: true, - Index: "missing.html", + Index: "missing.html", // that folder contains `index.html` }, whenURL: "/random", expectCode: http.StatusInternalServerError, }, { name: "ok, serve from http.FileSystem", givenConfig: &StaticConfig{ - Root: "_fixture", - Filesystem: os.DirFS(".."), + Root: "public", + Filesystem: os.DirFS("testdata/dist"), }, whenURL: "/", expectCode: http.StatusOK, - expectContains: "<title>Echo</title>", + expectContains: "<h1>Hello from index</h1>", }, { - name: "nok, URL encoded path traversal (single encoding)", - whenURL: "/%2e%2e%2fmiddleware/basic_auth.go", - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", + name: "nok, do not allow directory traversal (backslash - windows separator)", + whenURL: `/..\\private.txt`, + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, }, { - name: "nok, URL encoded path traversal (double encoding)", - whenURL: "/%252e%252e%252fmiddleware/basic_auth.go", - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", + name: "nok,do not allow directory traversal (slash - unix separator)", + whenURL: `/../private.txt`, + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, }, { - name: "nok, URL encoded path traversal (mixed encoding)", - whenURL: "/%2e%2e/middleware/basic_auth.go", - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", + name: "nok, URL encoded path traversal (single encoding, slash - unix separator)", + whenURL: "/%2e%2e%2fprivate.txt", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, }, { - name: "nok, backslash URL encoded", - whenURL: "/..%5c..%5cmiddleware/basic_auth.go", - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", + name: "nok, URL encoded path traversal (single encoding, backslash - windows separator)", + whenURL: "/%2e%2e%5cprivate.txt", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, + }, + { + name: "nok, URL encoded path traversal (double encoding, slash - unix separator)", + whenURL: "/%252e%252e%252fprivate.txt", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, + }, + { + name: "nok, URL encoded path traversal (double encoding, backslash - windows separator)", + whenURL: "/%252e%252e%255cprivate.txt", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, + }, + { + name: "nok, URL encoded path traversal (mixed encoding)", + whenURL: "/%2e%2e/private.txt", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, + }, + { + name: "nok, backslash URL encoded", + whenURL: "/..%5c..%5cprivate.txt", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, }, //{ // Under windows, %00 gets cleaned out by `http.ReadRequest` making this test to fail with different code // name: "nok, null byte injection", @@ -200,25 +218,26 @@ func TestStatic(t *testing.T) { // expectContains: "{\"message\":\"Internal Server Error\"}\n", //}, { - name: "nok, mixed backslash and forward slash traversal", - whenURL: "/..\\../middleware/basic_auth.go", - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", + name: "nok, mixed backslash and forward slash traversal", + whenURL: "/..\\../private.txt", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, }, { - name: "nok, trailing dots (Windows edge case)", - whenURL: "/../middleware/basic_auth.go...", - expectCode: http.StatusNotFound, - expectContains: "{\"message\":\"Not Found\"}\n", + name: "nok, trailing dots (Windows edge case)", + whenURL: "/../private.txt...", + expectCode: http.StatusNotFound, + expectContains: "{\"message\":\"Not Found\"}\n", + expectNotContains: `private file`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { e := echo.New() - e.Filesystem = os.DirFS("../") - config := StaticConfig{Root: "_fixture"} + config := StaticConfig{Root: "testdata/dist/public"} if tc.givenConfig != nil { config = *tc.givenConfig } @@ -247,169 +266,15 @@ func TestStatic(t *testing.T) { e.ServeHTTP(rec, req) assert.Equal(t, tc.expectCode, rec.Code) + responseBody := rec.Body.String() if tc.expectContains != "" { - responseBody := rec.Body.String() assert.Contains(t, responseBody, tc.expectContains) } - if tc.expectLength != "" { - assert.Equal(t, rec.Header().Get(echo.HeaderContentLength), tc.expectLength) - } - }) - } -} - -func TestStatic_GroupWithStatic(t *testing.T) { - var testCases = []struct { - name string - givenGroup string - givenPrefix string - givenRoot string - whenURL string - expectStatus int - expectHeaderLocation string - expectBodyStartsWith string - }{ - { - name: "ok", - givenPrefix: "/images", - givenRoot: "_fixture/images", - whenURL: "/group/images/walle.png", - expectStatus: http.StatusOK, - expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}), - }, - { - name: "No file", - givenPrefix: "/images", - givenRoot: "_fixture/scripts", - whenURL: "/group/images/bolt.png", - expectStatus: http.StatusNotFound, - expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", - }, - { - name: "Directory not found (no trailing slash)", - givenPrefix: "/images", - givenRoot: "_fixture/images", - whenURL: "/group/images/", - expectStatus: http.StatusNotFound, - expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", - }, - { - name: "Directory redirect", - givenPrefix: "/", - givenRoot: "_fixture", - whenURL: "/group/folder", - expectStatus: http.StatusMovedPermanently, - expectHeaderLocation: "/group/folder/", - expectBodyStartsWith: "", - }, - { - name: "Directory redirect", - givenPrefix: "/", - givenRoot: "_fixture", - whenURL: "/group/folder%2f..", - expectStatus: http.StatusMovedPermanently, - expectHeaderLocation: "/group/folder/../", - expectBodyStartsWith: "", - }, - { - name: "Prefixed directory 404 (request URL without slash)", - givenGroup: "_fixture", - givenPrefix: "/folder/", // trailing slash will intentionally not match "/folder" - givenRoot: "_fixture", - whenURL: "/_fixture/folder", // no trailing slash - expectStatus: http.StatusNotFound, - expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", - }, - { - name: "Prefixed directory redirect (without slash redirect to slash)", - givenGroup: "_fixture", - givenPrefix: "/folder", // no trailing slash shall match /folder and /folder/* - givenRoot: "_fixture", - whenURL: "/_fixture/folder", // no trailing slash - expectStatus: http.StatusMovedPermanently, - expectHeaderLocation: "/_fixture/folder/", - expectBodyStartsWith: "", - }, - { - name: "Directory with index.html", - givenPrefix: "/", - givenRoot: "_fixture", - whenURL: "/group/", - expectStatus: http.StatusOK, - expectBodyStartsWith: "<!doctype html>", - }, - { - name: "Prefixed directory with index.html (prefix ending with slash)", - givenPrefix: "/assets/", - givenRoot: "_fixture", - whenURL: "/group/assets/", - expectStatus: http.StatusOK, - expectBodyStartsWith: "<!doctype html>", - }, - { - name: "Prefixed directory with index.html (prefix ending without slash)", - givenPrefix: "/assets", - givenRoot: "_fixture", - whenURL: "/group/assets/", - expectStatus: http.StatusOK, - expectBodyStartsWith: "<!doctype html>", - }, - { - name: "Sub-directory with index.html", - givenPrefix: "/", - givenRoot: "_fixture", - whenURL: "/group/folder/", - expectStatus: http.StatusOK, - expectBodyStartsWith: "<!doctype html>", - }, - { - name: "do not allow directory traversal (backslash - windows separator)", - givenPrefix: "/", - givenRoot: "_fixture/", - whenURL: `/group/..\\middleware/basic_auth.go`, - expectStatus: http.StatusNotFound, - expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", - }, - { - name: "do not allow directory traversal (slash - unix separator)", - givenPrefix: "/", - givenRoot: "_fixture/", - whenURL: `/group/../middleware/basic_auth.go`, - expectStatus: http.StatusNotFound, - expectBodyStartsWith: "{\"message\":\"Not Found\"}\n", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - e := echo.New() - e.Filesystem = os.DirFS("../") // so we can access test files - - group := "/group" - if tc.givenGroup != "" { - group = tc.givenGroup - } - g := e.Group(group) - g.Static(tc.givenPrefix, tc.givenRoot) - - req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) - rec := httptest.NewRecorder() - - e.ServeHTTP(rec, req) - - assert.Equal(t, tc.expectStatus, rec.Code) - body := rec.Body.String() - if tc.expectBodyStartsWith != "" { - assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith)) - } else { - assert.Equal(t, "", body) + if tc.expectNotContains != "" { + assert.NotContains(t, responseBody, tc.expectNotContains) } - - if tc.expectHeaderLocation != "" { - assert.Equal(t, tc.expectHeaderLocation, rec.Header().Get(echo.HeaderLocation)) - } else { - _, ok := rec.Result().Header[echo.HeaderLocation] - assert.False(t, ok) + if tc.expectLength != "" { + assert.Equal(t, tc.expectLength, rec.Header().Get(echo.HeaderContentLength)) } }) } @@ -607,7 +472,7 @@ func TestStatic_DirectoryBrowsing(t *testing.T) { }, whenURL: "/assets", expectCode: http.StatusOK, - expectContains: `<a class="file" href="/assets/readme.md">readme.md</a>`, + expectContains: `<a class="file" href="readme.md">readme.md</a>`, expectNotContains: []string{ `<h1>Hello from index</h1>`, // should see the listing, not index.html contents `private.txt`, // file from the parent folder
middleware/static_windows.go+0 −34 removed@@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors - -package middleware - -import ( - "errors" - "io/fs" - "os" -) - -// We ignore these errors as there could be handler that matches request path. -// -// As of Go 1.20 filepath.Clean has different behaviour on OS related filesystems so we need to use path.Clean -// on Windows which has some caveats. The Open methods might return different errors than earlier versions and -// as of 1.20 path checks are more strict on the provided path and considers [UNC](https://en.wikipedia.org/wiki/Path_(computing)#UNC) -// paths with missing host etc parts as invalid. Previously it would result you `fs.ErrNotExist`. -// -// For 1.20@Windows we need to treat those errors the same as `fs.ErrNotExists` so we can continue handling -// errors in the middleware/handler chain. Otherwise we might end up with status 500 instead of finding a route -// or return 404 not found. -func isIgnorableOpenFileError(err error) bool { - if os.IsNotExist(err) { - return true - } - var pErr *fs.PathError - if errors.As(err, &pErr) { - err = pErr.Err - } - errTxt := err.Error() - return errTxt == "http: invalid or unsafe file path" || - errTxt == "invalid path" || - errTxt == "invalid argument" -}
middleware/testdata/dist/private.txt+1 −0 added@@ -0,0 +1 @@ +private file
middleware/testdata/dist/public/assets/readme.md+1 −0 added@@ -0,0 +1 @@ +This directory is used for the static middleware test
middleware/testdata/dist/public/assets/subfolder/subfolder.md+1 −0 added@@ -0,0 +1 @@ +file inside subfolder
middleware/testdata/dist/public/index.html+1 −0 added@@ -0,0 +1 @@ +<h1>Hello from index</h1>
middleware/testdata/dist/public/test.txt+1 −0 added@@ -0,0 +1 @@ +test.txt contents
middleware/testdata/private.txt+1 −0 added@@ -0,0 +1 @@ +private file
version.go+1 −1 modified@@ -5,5 +5,5 @@ package echo const ( // Version of Echo - Version = "5.0.2" + Version = "5.0.3" )
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-pgvm-wxw2-hrv9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25766ghsaADVISORY
- github.com/labstack/echo/commit/b1d443086ea27cf51345ec72a71e9b7e9d9ce5f1ghsax_refsource_MISCWEB
- github.com/labstack/echo/pull/2891ghsax_refsource_MISCWEB
- github.com/labstack/echo/security/advisories/GHSA-pgvm-wxw2-hrv9ghsax_refsource_CONFIRMWEB
- pkg.go.dev/vuln/GO-2026-4502ghsaWEB
News mentions
0No linked articles in our index yet.