Skip to content

Commit 2786cd6

Browse files
committed
add StrictQueryParamSep router option (#781)
1 parent db9d1d0 commit 2786cd6

File tree

5 files changed

+94
-21
lines changed

5 files changed

+94
-21
lines changed

mux.go

+22
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ type routeConf struct {
9797
// if true, the the http.Request context will not contain the router
9898
omitRouterFromContext bool
9999

100+
// If true, only ampersands (not semicolons) act as separators for
101+
// query-parameter pairs.
102+
strictQueryParamSep bool
103+
100104
// Manager for the variables from host and path.
101105
regexp routeRegexpGroup
102106

@@ -295,6 +299,24 @@ func (r *Router) OmitRouterFromContext(value bool) *Router {
295299
return r
296300
}
297301

302+
// StrictQueryParamSep defines which characters act as separators for
303+
// query-parameter pairs. The initial value is false, but beware: a future
304+
// version of this library will adopt true for the initial value.
305+
//
306+
// When true, only ampersands act as separators for query parameters.
307+
// This behavior complies with [the URL Living Standard].
308+
//
309+
// When false, both ampersands and semicolons act as separators for
310+
// query-parameter pairs. This contravenes the URL Living Standard and causes
311+
// interoperability problems that can lead to [security vulnerabilities].
312+
//
313+
// [security vulnerabilities]: https://github.com/gorilla/mux/issues/781
314+
// [the URL Living Standard]: https://url.spec.whatwg.org/#urlencoded-parsing
315+
func (r *Router) StrictQueryParamSep(value bool) *Router {
316+
r.strictQueryParamSep = value
317+
return r
318+
}
319+
298320
// UseEncodedPath tells the router to match the encoded original path
299321
// to the routes.
300322
// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".

mux_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import (
1212
"fmt"
1313
"io"
1414
"log"
15+
"maps"
1516
"net/http"
1617
"net/http/httptest"
1718
"net/url"
1819
"reflect"
20+
"strconv"
1921
"strings"
2022
"testing"
2123
"time"
@@ -2067,6 +2069,54 @@ func TestSkipClean(t *testing.T) {
20672069
}
20682070
}
20692071

2072+
func TestStrictQueryParamSep(t *testing.T) {
2073+
cases := []struct {
2074+
b bool
2075+
pairs []string
2076+
want map[string]string
2077+
}{
2078+
{
2079+
b: false,
2080+
pairs: []string{"foo", "{foo}", "bar", "{bar}", "baz", "{baz}"},
2081+
want: map[string]string{
2082+
"foo": "foo",
2083+
"bar": "bar",
2084+
"baz": "baz",
2085+
},
2086+
}, {
2087+
b: true,
2088+
pairs: []string{"foo", "{foo}", "baz", "{baz}"},
2089+
want: map[string]string{
2090+
"foo": "foo;bar=bar",
2091+
"baz": "baz",
2092+
},
2093+
},
2094+
}
2095+
for _, tc := range cases {
2096+
f := func(t *testing.T) {
2097+
var got map[string]string
2098+
handle := func(_ http.ResponseWriter, r *http.Request) {
2099+
got = Vars(r)
2100+
}
2101+
r := NewRouter()
2102+
if r.strictQueryParamSep {
2103+
t.Error("strickQueryParamSep should be false by default")
2104+
}
2105+
r.StrictQueryParamSep(tc.b)
2106+
r.HandleFunc("/", handle).Queries(tc.pairs...)
2107+
2108+
req := httptest.NewRequest("GET", "http://localhost/?foo=foo;bar=bar&baz=baz", nil)
2109+
res := NewRecorder()
2110+
r.ServeHTTP(res, req)
2111+
2112+
if !maps.Equal(got, tc.want) {
2113+
t.Errorf("unexpected query params: got %v; want %v", got, tc.want)
2114+
}
2115+
}
2116+
t.Run(strconv.FormatBool(tc.b), f)
2117+
}
2118+
}
2119+
20702120
// https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW
20712121
func TestSubrouterHeader(t *testing.T) {
20722122
expected := "func1 response"

regexp.go

+17-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package mux
66

77
import (
8-
"bytes"
98
"fmt"
109
"net/http"
1110
"net/url"
@@ -15,8 +14,9 @@ import (
1514
)
1615

1716
type routeRegexpOptions struct {
18-
strictSlash bool
19-
useEncodedPath bool
17+
strictSlash bool
18+
useEncodedPath bool
19+
strictQueryParamSep bool
2020
}
2121

2222
type regexpType int
@@ -194,6 +194,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
194194
if i := strings.Index(host, ":"); i != -1 {
195195
host = host[:i]
196196
}
197+
197198
}
198199
return r.regexp.MatchString(host)
199200
}
@@ -245,7 +246,8 @@ func (r *routeRegexp) getURLQuery(req *http.Request) string {
245246
return ""
246247
}
247248
templateKey := strings.SplitN(r.template, "=", 2)[0]
248-
val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey)
249+
strict := r.options.strictQueryParamSep
250+
val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey, strict)
249251
if ok {
250252
return templateKey + "=" + val
251253
}
@@ -254,34 +256,32 @@ func (r *routeRegexp) getURLQuery(req *http.Request) string {
254256

255257
// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0].
256258
// If key was not found, empty string and false is returned.
257-
func findFirstQueryKey(rawQuery, key string) (value string, ok bool) {
258-
query := []byte(rawQuery)
259-
for len(query) > 0 {
260-
foundKey := query
261-
if i := bytes.IndexAny(foundKey, "&;"); i >= 0 {
262-
foundKey, query = foundKey[:i], foundKey[i+1:]
259+
func findFirstQueryKey(rawQuery, key string, strict bool) (value string, ok bool) {
260+
for len(rawQuery) > 0 {
261+
foundKey := rawQuery
262+
if strict {
263+
foundKey, rawQuery, _ = strings.Cut(foundKey, "&")
264+
} else if i := strings.IndexAny(foundKey, "&;"); i >= 0 {
265+
foundKey, rawQuery = foundKey[:i], foundKey[i+1:]
263266
} else {
264-
query = query[:0]
267+
rawQuery = rawQuery[:0]
265268
}
266269
if len(foundKey) == 0 {
267270
continue
268271
}
269-
var value []byte
270-
if i := bytes.IndexByte(foundKey, '='); i >= 0 {
271-
foundKey, value = foundKey[:i], foundKey[i+1:]
272-
}
272+
foundKey, value, _ := strings.Cut(foundKey, "=")
273273
if len(foundKey) < len(key) {
274274
// Cannot possibly be key.
275275
continue
276276
}
277-
keyString, err := url.QueryUnescape(string(foundKey))
277+
keyString, err := url.QueryUnescape(foundKey)
278278
if err != nil {
279279
continue
280280
}
281281
if keyString != key {
282282
continue
283283
}
284-
valueString, err := url.QueryUnescape(string(value))
284+
valueString, err := url.QueryUnescape(value)
285285
if err != nil {
286286
continue
287287
}

regexp_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func Test_findFirstQueryKey(t *testing.T) {
5252
all, _ := url.ParseQuery(query)
5353
for key, want := range all {
5454
t.Run(key, func(t *testing.T) {
55-
got, ok := findFirstQueryKey(query, key)
55+
got, ok := findFirstQueryKey(query, key, false)
5656
if !ok {
5757
t.Error("Did not get expected key", key)
5858
}
@@ -81,7 +81,7 @@ func Benchmark_findQueryKey(b *testing.B) {
8181
b.ResetTimer()
8282
for i := 0; i < b.N; i++ {
8383
for key := range all {
84-
_, _ = findFirstQueryKey(query, key)
84+
_, _ = findFirstQueryKey(query, key, false)
8585
}
8686
}
8787
})

route.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,9 @@ func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error {
257257
}
258258
}
259259
rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{
260-
strictSlash: r.strictSlash,
261-
useEncodedPath: r.useEncodedPath,
260+
strictSlash: r.strictSlash,
261+
useEncodedPath: r.useEncodedPath,
262+
strictQueryParamSep: r.strictQueryParamSep,
262263
})
263264
if err != nil {
264265
return err

0 commit comments

Comments
 (0)