Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion regexp.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,15 @@ func (r *routeRegexp) getURLQuery(req *http.Request) string {
}

// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0].
// Only ampersands are recognized as query-parameter separators, in compliance
// with the URL Living Standard (https://url.spec.whatwg.org/#urlencoded-parsing)
// and Go's net/url since Go 1.17.
// If key was not found, empty string and false is returned.
func findFirstQueryKey(rawQuery, key string) (value string, ok bool) {
query := []byte(rawQuery)
for len(query) > 0 {
foundKey := query
if i := bytes.IndexAny(foundKey, "&;"); i >= 0 {
if i := bytes.IndexByte(foundKey, '&'); i >= 0 {
foundKey, query = foundKey[:i], foundKey[i+1:]
} else {
query = query[:0]
Expand Down
62 changes: 58 additions & 4 deletions regexp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ func Test_findFirstQueryKey(t *testing.T) {
"a=1&b=2",
"a=1&a=2&a=banana",
"ascii=%3Ckey%3A+0x90%3E",
"a=1;b=2",
"a=1&a=2;a=banana",
"a==",
"a=%2",
"a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30",
Expand All @@ -65,13 +63,70 @@ func Test_findFirstQueryKey(t *testing.T) {
}
}

// Test_findFirstQueryKey_semicolonIsNotSeparator verifies that semicolons
// are NOT treated as query parameter separators, in compliance with the
// URL Living Standard and Go's net/url since Go 1.17.
// See https://github.com/gorilla/mux/issues/781
func Test_findFirstQueryKey_semicolonIsNotSeparator(t *testing.T) {
// "a=1;b=2" should be treated as a single key-value pair where
// the key is "a" and the value is "1;b=2", not as two separate pairs.
t.Run("semicolon in value is not a separator", func(t *testing.T) {
_, ok := findFirstQueryKey("a=1;b=2", "b")
if ok {
t.Error("semicolon should not act as a query parameter separator")
}
val, ok := findFirstQueryKey("a=1;b=2", "a")
if !ok {
t.Error("expected to find key 'a'")
}
if val != "1;b=2" {
t.Errorf("expected value '1;b=2', got %q", val)
}
})

// Verify that the parser differential described in issue #781 is fixed:
// for "a;id=42&id=1", the key "id" should resolve to "1" (the
// ampersand-separated part), not "42" (the semicolon-separated part).
t.Run("no parser differential with net/url", func(t *testing.T) {
val, ok := findFirstQueryKey("a;id=42&id=1", "id")
if !ok {
t.Error("expected to find key 'id'")
}
if val != "1" {
t.Errorf("expected value '1', got %q", val)
}
// "a;id=42" should be treated as a single key (not split on semicolon).
_, ok = findFirstQueryKey("a;id=42&id=1", "a;id")
if !ok {
t.Error("expected to find key 'a;id' (semicolon is part of the key)")
}
})

// Additional case: semicolons mixed with ampersands should only split on ampersands.
t.Run("mixed semicolons and ampersands", func(t *testing.T) {
val, ok := findFirstQueryKey("foo=foo;bar=bar&baz=ding", "foo")
if !ok {
t.Error("expected to find key 'foo'")
}
if val != "foo;bar=bar" {
t.Errorf("expected value 'foo;bar=bar', got %q", val)
}
val, ok = findFirstQueryKey("foo=foo;bar=bar&baz=ding", "baz")
if !ok {
t.Error("expected to find key 'baz'")
}
if val != "ding" {
t.Errorf("expected value 'ding', got %q", val)
}
})
}

func Benchmark_findQueryKey(b *testing.B) {
tests := []string{
"a=1&b=2",
"ascii=%3Ckey%3A+0x90%3E",
"a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30",
"a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu",
"a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=",
}
for i, query := range tests {
b.Run(strconv.Itoa(i), func(b *testing.B) {
Expand All @@ -94,7 +149,6 @@ func Benchmark_findQueryKeyGoLib(b *testing.B) {
"ascii=%3Ckey%3A+0x90%3E",
"a=20&%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B&a=30",
"a=xxxxxxxxxxxxxxxx&bbb=YYYYYYYYYYYYYYY&cccc=ppppppppppppppppppp&ddddd=ttttttttttttttttt&a=uuuuuuuuuuuuu",
"a=;b=;c=;d=;e=;f=;g=;h=;i=,j=;k=",
}
for i, query := range tests {
b.Run(strconv.Itoa(i), func(b *testing.B) {
Expand Down
Loading