From 814d0530ebef164ff7ffff48040a9de25164aae5 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Thu, 5 Nov 2026 08:24:22 -0800 Subject: [PATCH] fix(gin): root the trailing-slash redirect after X-Forwarded-Prefix is sanitized Co-Authored-By: Claude Signed-off-by: Arpit Jain --- gin.go | 5 +++++ routes_test.go | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/gin.go b/gin.go index a160727597..1d59276b03 100644 --- a/gin.go +++ b/gin.go @@ -815,6 +815,11 @@ func redirectTrailingSlash(c *Context) { prefix = removeRepeatedChar(prefix, '/') p = prefix + "/" + req.URL.Path + // Keep the redirect target rooted at this server. Sanitizing can leave a + // prefix that is empty or has no leading slash, and then p starts with + // "//", which a client reads as a host, or with a path segment, which it + // resolves against the request path and so repeats the prefix. + p = "/" + strings.TrimLeft(p, "/") } req.URL.Path = p + "/" if length := len(p); length > 1 && p[length-1] == '/' { diff --git a/routes_test.go b/routes_test.go index a032a2b76a..1b85892d9d 100644 --- a/routes_test.go +++ b/routes_test.go @@ -260,7 +260,7 @@ func TestRouteRedirectTrailingSlash(t *testing.T) { assert.Equal(t, http.StatusMovedPermanently, w.Code) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../"}) - assert.Equal(t, "//path", w.Header().Get("Location")) + assert.Equal(t, "/path", w.Header().Get("Location")) assert.Equal(t, http.StatusMovedPermanently, w.Code) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "api/../../../"}) @@ -276,11 +276,11 @@ func TestRouteRedirectTrailingSlash(t *testing.T) { assert.Equal(t, http.StatusMovedPermanently, w.Code) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "https://gin-gonic.com/#"}) - assert.Equal(t, "https/gin-goniccom/https/gin-goniccom/path", w.Header().Get("Location")) + assert.Equal(t, "/https/gin-goniccom/path", w.Header().Get("Location")) assert.Equal(t, http.StatusMovedPermanently, w.Code) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "#api"}) - assert.Equal(t, "api/api/path", w.Header().Get("Location")) + assert.Equal(t, "/api/path", w.Header().Get("Location")) assert.Equal(t, http.StatusMovedPermanently, w.Code) w = PerformRequest(router, http.MethodGet, "/path/", header{Key: "X-Forwarded-Prefix", Value: "/nor-mal/#?a=1"}) @@ -291,6 +291,12 @@ func TestRouteRedirectTrailingSlash(t *testing.T) { assert.Equal(t, "/nor-mal/2e2e/path", w.Header().Get("Location")) assert.Equal(t, http.StatusMovedPermanently, w.Code) + // A prefix that sanitizes to nothing must not leave a Location starting + // with "//", which a client reads as the start of a host. + w = PerformRequest(router, http.MethodGet, "/path2", header{Key: "X-Forwarded-Prefix", Value: ".."}) + assert.Equal(t, "/path2/", w.Header().Get("Location")) + assert.Equal(t, http.StatusMovedPermanently, w.Code) + router.RedirectTrailingSlash = false w = PerformRequest(router, http.MethodGet, "/path/")