Files
upstream-chi/middleware/strip_test.go
upstream-mirror 72c9f2ee75
Some checks failed
Test / test (1.24.x, ubuntu-latest) (push) Successful in 2m56s
Test / test (1.25.x, ubuntu-latest) (push) Successful in 2m15s
Test / test (1.26.x, ubuntu-latest) (push) Successful in 2m23s
Test / test (1.27.x, ubuntu-latest) (push) Failing after 3m56s
Test / test (1.24.x, windows-latest) (push) Has been cancelled
Test / test (1.25.x, windows-latest) (push) Has been cancelled
Test / test (1.26.x, windows-latest) (push) Has been cancelled
Test / test (1.27.x, windows-latest) (push) Has been cancelled
snapshot of https://github.com/go-chi/chi.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:48:17 +05:00

330 lines
9.3 KiB
Go

package middleware
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/go-chi/chi/v5"
)
func TestStripSlashes(t *testing.T) {
r := chi.NewRouter()
// This middleware must be mounted at the top level of the router, not at the end-handler
// because then it'll be too late and will end up in a 404
r.Use(StripSlashes)
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("nothing here"))
})
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root"))
})
r.Route("/accounts/{accountID}", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "accountID")
w.Write([]byte(accountID))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
if _, resp := testRequest(t, ts, "GET", "/", nil); resp != "root" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "//", nil); resp != "root" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin", nil); resp != "admin" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/", nil); resp != "admin" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/nothing-here", nil); resp != "nothing here" {
t.Fatal(resp)
}
}
func TestStripSlashesInRoute(t *testing.T) {
r := chi.NewRouter()
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("nothing here"))
})
r.Get("/hi", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
r.Route("/accounts/{accountID}", func(r chi.Router) {
r.Use(StripSlashes)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("accounts index"))
})
r.Get("/query", func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "accountID")
w.Write([]byte(accountID))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
if _, resp := testRequest(t, ts, "GET", "/hi", nil); resp != "hi" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/hi/", nil); resp != "nothing here" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin", nil); resp != "accounts index" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/", nil); resp != "accounts index" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/query", nil); resp != "admin" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/query/", nil); resp != "admin" {
t.Fatal(resp)
}
}
func TestRedirectSlashes(t *testing.T) {
r := chi.NewRouter()
// This middleware must be mounted at the top level of the router, not at the end-handler
// because then it'll be too late and will end up in a 404
r.Use(RedirectSlashes)
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("nothing here"))
})
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root"))
})
r.Route("/accounts/{accountID}", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "accountID")
w.Write([]byte(accountID))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
if resp, body := testRequest(t, ts, "GET", "/", nil); body != "root" || resp.StatusCode != 200 {
t.Fatal(body, resp.StatusCode)
}
// NOTE: the testRequest client will follow the redirection..
if resp, body := testRequest(t, ts, "GET", "//", nil); body != "root" || resp.StatusCode != 200 {
t.Fatal(body, resp.StatusCode)
}
if resp, body := testRequest(t, ts, "GET", "/accounts/admin", nil); body != "admin" || resp.StatusCode != 200 {
t.Fatal(body, resp.StatusCode)
}
// NOTE: the testRequest client will follow the redirection..
if resp, body := testRequest(t, ts, "GET", "/accounts/admin/", nil); body != "admin" || resp.StatusCode != 200 {
t.Fatal(body, resp.StatusCode)
}
if resp, body := testRequest(t, ts, "GET", "/nothing-here", nil); body != "nothing here" || resp.StatusCode != 404 {
t.Fatal(body, resp.StatusCode)
}
// Ensure redirect Location url is correct
{
resp, body := testRequestNoRedirect(t, ts, "GET", "/accounts/someuser/", nil)
if resp.StatusCode != 301 {
t.Fatal(body, resp.StatusCode)
}
location := resp.Header.Get("Location")
if location != "/accounts/someuser" {
t.Fatalf("invalid redirection, should be /accounts/someuser")
}
}
// Ensure query params are kept in tact upon redirecting a slash
{
resp, body := testRequestNoRedirect(t, ts, "GET", "/accounts/someuser/?a=1&b=2", nil)
if resp.StatusCode != 301 {
t.Fatal(body, resp.StatusCode)
}
location := resp.Header.Get("Location")
if location != "/accounts/someuser?a=1&b=2" {
t.Fatalf("invalid redirection, should be /accounts/someuser?a=1&b=2")
}
}
// Ensure that we don't redirect to 'evil.com', but rather to 'server.url/evil.com/'
{
paths := []string{"//evil.com/", "///evil.com/"}
for _, p := range paths {
resp, body := testRequest(t, ts, "GET", p, nil)
if u, err := url.Parse(ts.URL); err != nil && resp.Request.URL.Host != u.Host {
t.Fatalf("host should remain the same. got: %q, want: %q", resp.Request.URL.Host, ts.URL)
}
if body != "nothing here" || resp.StatusCode != 404 {
t.Fatal(body, resp.StatusCode)
}
}
}
// Ensure that we don't redirect to 'evil.com', but rather to 'server.url/evil.com/'
{
resp, body := testRequest(t, ts, "GET", "//evil.com/", nil)
if u, err := url.Parse(ts.URL); err != nil && resp.Request.URL.Host != u.Host {
t.Fatalf("host should remain the same. got: %q, want: %q", resp.Request.URL.Host, ts.URL)
}
if body != "nothing here" || resp.StatusCode != 404 {
t.Fatal(body, resp.StatusCode)
}
}
}
// This tests a http.Handler that is not chi.Router
// In these cases, the routeContext is nil
func TestStripSlashesWithNilContext(t *testing.T) {
r := http.NewServeMux()
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root"))
})
r.HandleFunc("/accounts", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("accounts"))
})
r.HandleFunc("/accounts/admin", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("admin"))
})
ts := httptest.NewServer(StripSlashes(r))
defer ts.Close()
if _, resp := testRequest(t, ts, "GET", "/", nil); resp != "root" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "//", nil); resp != "root" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts", nil); resp != "accounts" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/", nil); resp != "accounts" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin", nil); resp != "admin" {
t.Fatal(resp)
}
if _, resp := testRequest(t, ts, "GET", "/accounts/admin/", nil); resp != "admin" {
t.Fatal(resp)
}
}
func TestStripPrefix(t *testing.T) {
r := chi.NewRouter()
r.Use(StripPrefix("/api"))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("api root"))
})
r.Get("/accounts", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("api accounts"))
})
r.Get("/accounts/{accountID}", func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "accountID")
w.Write([]byte(accountID))
})
ts := httptest.NewServer(r)
defer ts.Close()
if _, resp := testRequest(t, ts, "GET", "/api/", nil); resp != "api root" {
t.Fatalf("got: %q, want: %q", resp, "api root")
}
if _, resp := testRequest(t, ts, "GET", "/api/accounts", nil); resp != "api accounts" {
t.Fatalf("got: %q, want: %q", resp, "api accounts")
}
if _, resp := testRequest(t, ts, "GET", "/api/accounts/admin", nil); resp != "admin" {
t.Fatalf("got: %q, want: %q", resp, "admin")
}
if _, resp := testRequest(t, ts, "GET", "/api-nope/", nil); resp != "404 page not found\n" {
t.Fatalf("got: %q, want: %q", resp, "404 page not found\n")
}
}
func TestRedirectSlashes_PreventBackslashRelativeOpenRedirect(t *testing.T) {
h := RedirectSlashes(http.NotFoundHandler())
tests := []struct {
name string
target string
}{
{
name: `raw backslash: /\evil.com/`,
target: `/\evil.com/`,
},
{
name: `encoded backslash: /%5Cevil.com/`,
target: "/%5Cevil.com/",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://example.test"+tc.target, nil)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
res := rr.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusMovedPermanently {
t.Fatalf("expected %d, got %d", http.StatusMovedPermanently, res.StatusCode)
}
loc := res.Header.Get("Location")
if loc == "" {
t.Fatalf("expected Location header to be set")
}
// The core security assertions:
if strings.Contains(loc, `\`) {
t.Fatalf("Location must not contain backslashes: %q", loc)
}
if strings.HasPrefix(loc, "//") {
t.Fatalf("Location must not be protocol-relative: %q", loc)
}
if !strings.HasPrefix(loc, "/") {
t.Fatalf("Location must be an absolute-path reference starting with '/': %q", loc)
}
// Optional stronger assertion if your middleware normalizes to /evil.com exactly:
// (Keep or remove depending on your chosen behavior.)
if loc != "/evil.com" {
t.Fatalf("expected Location %q, got %q", "/evil.com", loc)
}
})
}
}