mirror of
https://github.com/labstack/echo.git
synced 2026-06-18 01:05:43 +02:00
a9ede66a5a
* 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>
65 lines
2.3 KiB
Go
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)
|
|
}
|
|
}
|