VYPR
Moderate severityNVD Advisory· Published Feb 19, 2026· Updated Feb 19, 2026

Echo has a Windows path traversal via backslash in middleware.Static default filesystem

CVE-2026-25766

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.

PackageAffected versionsPatched versions
github.com/labstack/echo/v5Go
>= 5.0.0, < 5.0.35.0.3

Affected products

2
  • Nextapp/Echollm-fuzzy
    Range: >=5.0.0 <=5.0.2
  • labstack/echov5
    Range: >= 5.0.0, < 5.0.3

Patches

1
b1d443086ea2

Merge pull request #2891 from aldas/fix_staticmw

https://github.com/labstack/echoMartti T.Feb 6, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.