← Research blog

RESEARCH / AUTHORIZATION

What Go's Router Does Before a Handler Runs

A local ServeMux trace establishes when a path redirects, so an application's authorization can be assessed against the path it actually handles.

Map the request before looking for a policy gap

An HTTP path can change meaning between the socket, router, handler, and application object lookup. In Go 1.24.0, ServeMux.findHandler treats non-CONNECT paths differently from CONNECT: the ordinary path is cleaned and may receive a redirect before a handler runs. The cleaning function and the later matcher are separate steps. An authorization rule outside the mux or inside a handler may therefore observe a different stage of the request.

RECONSTRUCTED SOURCE MAPRequest path through ServeMux
Go standard library go1.24.0

Scroll the diagram horizontally

A request path enters ServeMux cleaning. A noncanonical path can redirect before a handler runs; a canonical path can reach route matching and the application's resource lookup and policy.
PINNED SOURCE

We checked this boundary with the site’s reproducible Go fixture: the canonical path reached the handler, while /private/../private/report and /private//report each returned 301 with Location: /private/report and did not increase the handler hit count. The exact input, status, location, and count appear below. This observation matters because treating a redirect as if the original request had reached the protected resource would produce a false bypass claim.

The next security question belongs to the actual application: which path representation does its policy authorize, which object does its resolver select, and is policy checked again on any redirected request? The Go standard library fixture establishes routing behavior only. It contains no authorization middleware or sensitive operation.

What the router actually does

In Go 1.24.0, net/http.ServeMux cleans a non-CONNECT request path before matching it:

path = cleanPath(path)

The source at lines 2678–2697 passes that path to matchOrRedirect. If it differs from the incoming escaped path, the mux returns a redirect to the cleaned path. The redirect is an important part of the observation: it can cause a new request, rather than silently executing a handler under the original spelling.

The identity question

Names have more than one representation. A rule can inspect the request spelling, a router can select a cleaned path, and an application can resolve that path to an object. Reviewing an authorization boundary means stating which identity the rule protects and where that identity is established.

The Go source shows routing behavior, not an authorization decision. Assessing a service requires reading its policy and resolver together.

Required request trace

For a service under review, record the input path, any redirect, the final matched route, the resolved resource, and the policy decision. Compare a canonical spelling with an alternate spelling that the actual router accepts. Only a same-path trace can show whether the policy and execution refer to different objects. Assumed equivalence between strings is insufficient.

Reproduce the redirect, then place the policy boundary

We ran a small local httptest fixture with the Go 1.24.0 toolchain. It registers one handler at /private/report, sends three requests directly to ServeMux, and counts handler invocations. The recorded output was:

path=/private/report status=200 location="" handler_hits=1
path=/private/../private/report status=301 location="/private/report" handler_hits=1
path=/private//report status=301 location="/private/report" handler_hits=1

The alternate paths produced redirects and did not invoke the handler in that first request. This matches the source’s cleanPath and findHandler redirect branch. The fixture contains no authorization logic; it isolates the routing boundary.

Now compare two possible application placements:

  1. A policy before ServeMux may inspect the original request spelling. If ServeMux redirects it, the first request ends; a later request must be checked on its own route.
  2. A policy after route matching may inspect the selected handler or resolved object before the handler performs an effect.

The same string can play different roles at those stages. Neither position is automatically unsafe. The reviewer must show exactly which representation each rule protects and whether the request that executes the operation passed the intended policy check.

The Go implementation also treats CONNECT differently: findHandler does not canonicalize that method’s path. Method, escaped path, raw query, redirect following, and any outer reverse proxy are therefore part of a credible end-to-end trace. A toy comparison of two path strings without the router and the final policy decision cannot establish impact.

Scope of the local result

The fixture uses httptest.NewRecorder() and calls mux.ServeHTTP once for each input. It deliberately does not follow the returned Location. A browser that follows the redirect makes a second request to /private/report, which must be evaluated by whatever policy is installed on that second request. The unchanged handler_hits=1 in both redirect rows is the direct evidence that the first, noncanonical requests did not run the handler. A report that collapses redirect and destination into one execution would misdescribe the sequence.

To reproduce the observation, run GOTOOLCHAIN=go1.24.0 go run static/research/experiments/servemux_paths.go from this site’s source tree. The program itself is short enough to audit: it registers one path, sends the three requests, and prints status, Location, and the cumulative hit count. It is not a model of a reverse proxy, an access-control middleware, or a storage resolver. Those layers would have to be added for any finding about mismatched identities.

A stronger authorization experiment compares two complete executions: a canonical request and an alternate spelling that the real stack accepts. Record the initial identity string, normalized route, resource identifier after resolution, policy subject, policy result, handler entry, and committed effect. Keep the starting state identical. If the alternate spelling redirects and the policy is reevaluated on the destination request, the alleged bypass is falsified for that route. If different components truly authorize and operate on different objects, the trace will show the exact boundary instead of relying on a suggestive URL alone.

MORE RESEARCH

Explore more research.

Research blog