diff --git a/gin.go b/gin.go index d71086d1..8eac7277 100644 --- a/gin.go +++ b/gin.go @@ -11,9 +11,9 @@ import ( "net/http" "os" "path" - "regexp" "strings" "sync" + "unicode" "github.com/gin-gonic/gin/internal/bytesconv" filesystem "github.com/gin-gonic/gin/internal/fs" @@ -48,11 +48,6 @@ var defaultTrustedCIDRs = []*net.IPNet{ }, } -var ( - regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+") - regRemoveRepeatedChar = regexp.MustCompile("/{2,}") -) - // HandlerFunc defines the handler used by gin middleware as return value. type HandlerFunc func(*Context) @@ -776,8 +771,8 @@ func redirectTrailingSlash(c *Context) { req := c.Request p := req.URL.Path if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { - prefix = regSafePrefix.ReplaceAllString(prefix, "") - prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/") + prefix = sanitizePathChars(prefix) + prefix = removeRepeatedSlash(prefix) p = prefix + "/" + req.URL.Path } @@ -788,6 +783,39 @@ func redirectTrailingSlash(c *Context) { redirectRequest(c) } +// sanitizePathChars removes unsafe characters from path strings, +// keeping only letters, numbers, forward slashes, and hyphens. +func sanitizePathChars(s string) string { + return strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '/' || r == '-' { + return r + } + return -1 + }, s) +} + +// removeRepeatedSlash removes consecutive forward slashes from a string, +// replacing sequences of multiple slashes with a single slash. +func removeRepeatedSlash(s string) string { + if !strings.Contains(s, "//") { + return s + } + + var sb strings.Builder + sb.Grow(len(s) - 1) + prevChar := rune(0) + + for _, r := range s { + if r == '/' && prevChar == '/' { + continue + } + sb.WriteRune(r) + prevChar = r + } + + return sb.String() +} + func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { req := c.Request rPath := req.URL.Path diff --git a/gin_test.go b/gin_test.go index cee1f3cc..8d006365 100644 --- a/gin_test.go +++ b/gin_test.go @@ -1012,3 +1012,34 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) { assert.Equal(t, "ok", w.Body.String()) } } + +func TestRemoveRepeatedSlash(t *testing.T) { + testCases := []struct { + name string + str string + want string + }{ + { + name: "noSlash", + str: "abc", + want: "abc", + }, + { + name: "withSlash", + str: "/a/b/c/", + want: "/a/b/c/", + }, + { + name: "withRepeatedSlash", + str: "/a//b///c////", + want: "/a/b/c/", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res := removeRepeatedSlash(tc.str) + assert.Equal(t, tc.want, res) + }) + } +}