VYPR
Moderate severityNVD Advisory· Published Jun 29, 2021· Updated Aug 3, 2024

URL Redirection to Untrusted Site ('Open Redirect') in github.com/AndrewBurian/powermux

CVE-2021-32721

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.

PackageAffected versionsPatched versions
github.com/AndrewBurian/powermuxGo
< 1.1.11.1.1

Affected products

3

Patches

1
5e60a8a0372b

Added fix for zero-length segments (ie '//')

https://github.com/AndrewBurian/powermuxAndrewBurianJun 22, 2021via ghsa
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

News mentions

0

No linked articles in our index yet.