1
0
mirror of https://github.com/labstack/echo.git synced 2026-06-18 01:05:43 +02:00
Files
echo/middleware/static_percent_test.go
Vishal Rana a9ede66a5a fix(middleware/static): don't double-unescape request path (#2599) (#3006)
* fix(middleware/static): don't double-unescape request path (#2599)

http.Request.URL.Path is already decoded by net/http, but the static
middleware unescaped it again by default, so files whose names contain a
percent sign were not downloadable ("/100%25.txt" -> "invalid URL escape").

Default pathUnescape to false; only the wildcard param from a group route
(set explicitly below) may still be escaped and is handled by the existing
DisablePathUnescaping toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(static): add percent-encoded traversal-protection case (#2599)

Companion test confirming that defaulting pathUnescape to false does not
weaken traversal protection — single-, double-, and dot-encoded "../" all
stay within the served root.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:22:39 -07:00

65 lines
2.3 KiB
Go

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"testing/fstest"
"github.com/labstack/echo/v5"
"github.com/stretchr/testify/assert"
)
// Regression test for #2599: a file whose name contains a percent sign must be
// downloadable. http.Request.URL.Path is already decoded by net/http, so the
// static middleware must not unescape it a second time (which turned
// "/100%25.txt" into an "invalid URL escape" error or a missing file).
func TestStatic_servesFileWithPercentInName(t *testing.T) {
e := echo.New()
e.Use(StaticWithConfig(StaticConfig{
Root: ".",
Filesystem: fstest.MapFS{
"100%.txt": &fstest.MapFile{Data: []byte("hundred percent")},
"foo%20bar.txt": &fstest.MapFile{Data: []byte("literal percent twenty")},
},
}))
cases := map[string]string{
"/100%25.txt": "hundred percent",
"/foo%2520bar.txt": "literal percent twenty",
}
for url, want := range cases {
req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code, "GET %s should serve the file", url)
assert.Equal(t, want, rec.Body.String(), "GET %s should return the file contents", url)
}
}
// Companion to #2599: not unescaping the already-decoded path must not weaken
// traversal protection. A percent-encoded "../" must not escape the served root
// (and notably must not be re-assembled from double-encoded input, as the old
// double-unescape could do).
func TestStatic_percentEncodedTraversalIsBlocked(t *testing.T) {
e := echo.New()
e.Use(StaticWithConfig(StaticConfig{
Root: "public",
Filesystem: fstest.MapFS{
"public/page.txt": &fstest.MapFile{Data: []byte("public page")},
"secret.txt": &fstest.MapFile{Data: []byte("SECRET")},
},
}))
for _, url := range []string{"/..%2fsecret.txt", "/..%252fsecret.txt", "/%2e%2e%2fsecret.txt"} {
req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
assert.NotEqual(t, http.StatusOK, rec.Code, "GET %s must not escape the served root", url)
assert.NotContains(t, rec.Body.String(), "SECRET", "GET %s must not leak files above the root", url)
}
}