diff --git a/router.go b/router.go index 2a23f15..a4306e0 100644 --- a/router.go +++ b/router.go @@ -132,6 +132,15 @@ func (t *TreeMux) lookup(w http.ResponseWriter, r *http.Request) (result LookupR unescapedPath = strings.ToLower(unescapedPath) } + // A malformed request line can leave path empty or without a leading + // slash, e.g. a RequestURI of "?" collapses to "" once the query string + // is stripped. The searches below index path with path[pathLen-1] and + // path[1:], so anything that is not a rooted path cannot match a route and + // would otherwise panic. Treat it as not found. + if pathLen == 0 || path[0] != '/' { + return + } + trailingSlash := path[pathLen-1] == '/' && pathLen > 1 if trailingSlash && t.RedirectTrailingSlash { path = path[:pathLen-1] diff --git a/router_test.go b/router_test.go index 818d10c..5681f27 100644 --- a/router_test.go +++ b/router_test.go @@ -1060,6 +1060,30 @@ func TestLookup(t *testing.T) { tryLookup("POST", "/user/dimfeld/", true, http.StatusTemporaryRedirect) } +// A RequestURI that collapses to an empty path once the query string is +// stripped (for example a bare "?") used to index the path slice out of range +// and panic. Such malformed requests, common from vulnerability scanners, +// should just report not found. +func TestLookupMalformedRequestURI(t *testing.T) { + router := New() + router.GET("/foo", simpleHandler) + + for _, uri := range []string{"?", "*", "foo"} { + r, _ := http.NewRequest("GET", "http://example.com/foo", nil) + r.RequestURI = uri + r.URL.RawQuery = "" + w := &mockResponseWriter{} + + lr, found := router.Lookup(w, r) + if found { + t.Errorf("RequestURI %q expected not found", uri) + } + if lr.StatusCode != http.StatusNotFound { + t.Errorf("RequestURI %q expected status %d, saw %d", uri, http.StatusNotFound, lr.StatusCode) + } + } +} + func TestRedirectEscapedPath(t *testing.T) { router := New()