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
330 lines
9.3 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|