Skip to content

middleware/redirect: prevent open redirect (CWE-601) via malformed Host, unsanitized RequestURI, and non-HTTP schemes #3217

Closed

Description

@martinyonatann

The redirect middlewares in middleware/redirect.go (HTTPSRedirect, HTTPSWWWRedirect, HTTPSNonWWWRedirect, WWWRedirect, NonWWWRedirect) construct the target Location URL by directly concatenating scheme, req.Host, and req.RequestURI without validating the host or sanitizing the URI.

This introduces CWE-601: URL Redirection to Untrusted Site ('Open Redirect') and related redirect construction issues:

  1. Open Redirect (CWE-601) via bare www. or empty Host header:
    In NonWWWRedirect and HTTPSNonWWWRedirect, stripping "www." from a request with Host: www. (or Host: www.:80, or an empty Host header in HTTPSRedirect) leaves an empty host (""). Combined with a request path like /attacker.com/login, the middleware emits Location: https:///attacker.com/login (or http:///attacker.com/login), which browsers normalize to https://attacker.com/login. Similarly, a Host header containing authority delimiters (e.g. attacker.com@example.com or attacker.com/path) is concatenated unvalidated into the redirect URL.
  2. Open Redirect / Path Manipulation via unsanitized req.RequestURI:
    Unlike AddTrailingSlash / RemoveTrailingSlash in middleware/slash.go, redirect.go does not pass the URI through sanitizeURI() (leaving double slashes like //attacker.com and control characters unescaped) and does not handle cases where req.RequestURI is empty or in absolute-form (e.g., GET http://example.com/path HTTP/1.1).
  3. Case-sensitive www. prefix matching:
    strings.HasPrefix(host, "www.") is case-sensitive, so Host: WWW.example.com is redirected to https://www.WWW.example.com by HTTPSWWWRedirect / WWWRedirect and is not stripped by NonWWWRedirect.
  4. WebSocket scheme propagation (ws / wss):
    c.Scheme() can return "ws" or "wss" when extracted from forwarding headers (X-Forwarded-Proto: ws), causing WWWRedirect / NonWWWRedirect to emit Location: ws://... instead of http:// or https://.

How to Reproduce?

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

func TestRedirectOpenRedirectCWE601(t *testing.T) {
	e := echo.New()
	e.Pre(middleware.HTTPSNonWWWRedirect())
	e.GET("/*", func(c *echo.Context) error {
		return c.String(http.StatusOK, "OK")
	})

	req := httptest.NewRequest(http.MethodGet, "/attacker.com/login", nil)
	req.Host = "www."
	rec := httptest.NewRecorder()

	e.ServeHTTP(rec, req)

	// Redirects to "https:///attacker.com/login", which browsers resolve to https://attacker.com/login (CWE-601)
	if loc := rec.Header().Get(echo.HeaderLocation); loc == "https:///attacker.com/login" {
		t.Errorf("vulnerable to CWE-601 open redirect, got Location: %s", loc)
	}
}

Proposed Solution

  • Validate req.Host before redirecting (ensure non-empty host after stripping www., disallow leading : or ., control characters, and /\@?# delimiters).
  • Match and strip the www. prefix case-insensitively.
  • Fall back to req.URL.RequestURI() when req.RequestURI is empty or does not start with /, and sanitize with sanitizeURI().
  • Normalize "ws" -> "http" and "wss" -> "https" in the redirect middleware.

References

Version/commit

v5.4.4

Activity

  1. added 2 commits that reference this issue on Nov 12, 2026
    2cf7d4d
    bf9d756
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions