URL Redirection to Untrusted Site ('Open Redirect') in github.com/AndrewBurian/powermux
Description
PowerMux is a drop-in replacement for Go's http.ServeMux. In PowerMux versions prior to 1.1.1, attackers may be able to craft phishing links and other open redirects by exploiting the trailing slash redirection feature. This may lead to users being redirected to untrusted sites after following an attacker crafted link. The issue is resolved in v1.1.1. There are no existing workarounds.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An open redirect in PowerMux <1.1.1 allows attackers to craft phishing links by exploiting trailing slash redirection with double slashes.
Vulnerability
In PowerMux, a drop-in replacement for Go's http.ServeMux, versions prior to 1.1.1 contain an open redirect vulnerability in the trailing slash redirection feature. The execute function in route.go [3] performs a trailing slash redirect (e.g., /foo/ → /foo) without validating that the resulting path does not contain malicious user-controlled sequences. Attackers can inject double slashes (//) in the URL path, causing the redirect to point to an attacker-controlled scheme or hostname. Versions affected are all those before v1.1.1 [1][2][4].
Exploitation
An attacker must craft a URL containing a path with consecutive forward slashes followed by a trailing slash (e.g., https://example.com//evil.com/). When PowerMux processes the request, the trailing slash redirect logic in execute() [3] trims the trailing slash but does not sanitize the double-slash sequence, producing a redirect target of //evil.com/ which many user agents interpret as a protocol-relative redirect to https://evil.com. No authentication, special network position, or user interaction beyond clicking the crafted link is required [1][2][4].
Impact
Upon following the attacker-crafted link, users are redirected to an untrusted site under the attacker's control. This enables phishing attacks, malware distribution, or theft of credentials. The impact is a high-severity open redirect (CWE-601) with a CVSS score of 6.1 (AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N) [2].
Mitigation
The issue is resolved in PowerMux v1.1.1 [1], which adds a check in execute() to reject paths containing // with a 400 Bad Request response [3]. Users should upgrade to v1.1.1 or later. No workarounds exist for unpatched versions [2][4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/AndrewBurian/powermuxGo | < 1.1.1 | 1.1.1 |
Affected products
3- Range: <1.1.1
- AndrewBurian/powermuxv5Range: < 1.1.1
Patches
15e60a8a0372bAdded fix for zero-length segments (ie '//')
5 files changed · +102 −11
go.mod+2 −0 modified@@ -1 +1,3 @@ module github.com/AndrewBurian/powermux + +go 1.14
handlers.go+13 −0 modified@@ -1,6 +1,7 @@ package powermux import ( + "io" "net/http" "strings" ) @@ -51,3 +52,15 @@ func (r *Route) methodNotAllowed() http.Handler { return nil } + +type badRequestHandler string + +func (brh badRequestHandler) ServeHTTP(res http.ResponseWriter, _ *http.Request) { + res.Header().Set("Content-Type", "text/plain") + res.WriteHeader(http.StatusBadRequest) + io.WriteString(res, string(brh)) +} + +func (r *Route) badRequest(errMsg string) http.Handler{ + return badRequestHandler(errMsg) +} \ No newline at end of file
route.go+14 −2 modified@@ -135,7 +135,13 @@ func newRoute() *Route { // a route. func (r *Route) execute(ex *routeExecution, method, pattern string) { + if strings.Contains(pattern, "//") { + ex.handler = r.badRequest("Invalid path") + return + } + pathParts := pathPartsPool.Get().([]string)[0:0] + defer pathPartsPool.Put(pathParts) pathParts = append(pathParts, "") start := 1 for i := 1; i < len(pattern); i++ { @@ -146,6 +152,14 @@ func (r *Route) execute(ex *routeExecution, method, pattern string) { } } + // redirect trailing slashes + if pattern != "/" && strings.HasSuffix(pattern, "/") { + target := strings.TrimSuffix(pattern, "/") + ex.handler = http.RedirectHandler(target, http.StatusPermanentRedirect) + ex.pattern = target + return + } + // get the trailing path param if pattern != "/" { pathParts = append(pathParts, pattern[start:]) @@ -154,8 +168,6 @@ func (r *Route) execute(ex *routeExecution, method, pattern string) { // Fill the execution r.getExecution(method, pathParts, ex) - // return path parts - pathPartsPool.Put(pathParts) } // getExecution is a recursive step in the tree traversal. It checks to see if this node matches,
routematrix_test.go+73 −0 added@@ -0,0 +1,73 @@ +package powermux + +import ( + "fmt" + "testing" + "net/http" + "net/http/httptest" + "bytes" +) + +type RouteTest struct { + route string + body string + method string + expectCode int + expectLocation string +} + +var routeTests = []RouteTest{ + { + route: "/", + expectCode: http.StatusNotFound, + }, + { + route: "/foo/", + expectLocation: "/foo", + }, + { + route: "//example.com/", + expectCode: http.StatusBadRequest, + }, + { + route: "/foo//bar", + expectCode: http.StatusBadRequest, + }, +} + +func TestMuxRoutes(t *testing.T) { + + mux := NewServeMux() + for _, tt := range routeTests { + + // sane defaults + if tt.expectCode == 0 { + if tt.expectLocation != "" { + tt.expectCode = http.StatusPermanentRedirect + } else { + tt.expectCode = http.StatusOK + } + } + if tt.method == "" { + tt.method = http.MethodGet + } + + t.Run(fmt.Sprintf("%s%s", tt.method, tt.route), func(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest(tt.method, tt.route, bytes.NewBufferString(tt.body)) + mux.ServeHTTP(rec, req) + + if rec.Code != tt.expectCode { + t.Fatalf("Unexpected status code, expected=%d actual=%d", tt.expectCode, rec.Code) + } + + if tt.expectLocation != "" && (rec.Code / 100) != 3 { + t.Fatalf("Expected redirect, instead got code %d", rec.Code) + } + + if tt.expectLocation != "" && tt.expectLocation != rec.Header().Get("Location") { + t.Fatalf("Mismatched redirect. expected=%s, actual=%s", tt.expectLocation, rec.Header().Get("Location")) + } + }) + } +}
servemux.go+0 −9 modified@@ -4,7 +4,6 @@ import ( "bytes" "context" "net/http" - "strings" ) // ServeMux is the multiplexer for http requests @@ -68,14 +67,6 @@ func NewServeMux() *ServeMux { func (s *ServeMux) getAll(r *http.Request, ex *routeExecution) { path := r.URL.EscapedPath() - // Check for redirect - if path != "/" && strings.HasSuffix(path, "/") { - r.URL.Path = strings.TrimRight(path, "/") - ex.handler = http.RedirectHandler(r.URL.RequestURI(), http.StatusPermanentRedirect) - ex.pattern = r.URL.EscapedPath() - return - } - // fill it if route, ok := s.hostRoutes[r.URL.Host]; ok { route.execute(ex, r.Method, path)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-mj9r-wwm8-7q52ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-32721ghsaADVISORY
- github.com/AndrewBurian/powermux/commit/5e60a8a0372b35a898796c2697c40e8daabed8e9ghsaWEB
- github.com/AndrewBurian/powermux/pull/42ghsaWEB
- github.com/AndrewBurian/powermux/security/advisories/GHSA-mj9r-wwm8-7q52ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.