From 19b39dd08a32cbc9c9bbbe136f1210f2f3717d57 Mon Sep 17 00:00:00 2001 From: Erik Dubbelboer Date: Mon, 27 Apr 2026 12:28:30 +0900 Subject: [PATCH] Sanitize redirect Location header to prevent CRLF injection (#2186) Route RequestCtx.Redirect Location updates through the canonical response header setter so CR and LF bytes are normalized before serialization. Add regression coverage for query-only and fragment-only redirects containing CRLF, and verify the serialized response cannot emit an injected header line. --- server.go | 2 +- server_test.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server.go b/server.go index e84c222..b3c24d7 100644 --- a/server.go +++ b/server.go @@ -1450,7 +1450,7 @@ func (ctx *RequestCtx) RedirectBytes(uri []byte, statusCode int) { } func (ctx *RequestCtx) redirect(uri []byte, statusCode int) { - ctx.Response.Header.setNonSpecial(strLocation, uri) + ctx.Response.Header.SetCanonical(strLocation, uri) statusCode = getRedirectStatusCode(statusCode) ctx.Response.SetStatusCode(statusCode) } diff --git a/server_test.go b/server_test.go index 25da478..094e557 100644 --- a/server_test.go +++ b/server_test.go @@ -567,12 +567,14 @@ func TestRequestCtxRedirect(t *testing.T) { testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "#aaa", "http://qqq/foo/bar?baz=111#aaa") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "?abc=de&f", "http://qqq/foo/bar?abc=de&f") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "?abc=de&f#sf", "http://qqq/foo/bar?abc=de&f#sf") + testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "?\r\nInjected: yes", "http://qqq/foo/bar? Injected: yes") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "x.html", "http://qqq/foo/x.html") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "x.html?a=1", "http://qqq/foo/x.html?a=1") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "x.html#aaa=bbb&cc=ddd", "http://qqq/foo/x.html#aaa=bbb&cc=ddd") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "x.html?b=1#aaa=bbb&cc=ddd", "http://qqq/foo/x.html?b=1#aaa=bbb&cc=ddd") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "/x.html", "http://qqq/x.html") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "/x.html#aaa=bbb&cc=ddd", "http://qqq/x.html#aaa=bbb&cc=ddd") + testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "#\r\nInjected: yes", "http://qqq/foo/bar?baz=111# Injected: yes") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "http://foo.bar/baz", "http://foo.bar/baz") testRequestCtxRedirect(t, "http://qqq/foo/bar?baz=111", "https://foo.bar/baz", "https://foo.bar/baz") testRequestCtxRedirect(t, "https://foo.com/bar?aaa", "//google.com/aaa?bb", "https://google.com/aaa?bb") @@ -595,6 +597,12 @@ func testRequestCtxRedirect(t *testing.T, origURL, redirectURL, expectedURL stri if string(loc) != expectedURL { t.Fatalf("unexpected redirect url %q. Expecting %q. origURL=%q, redirectURL=%q", loc, expectedURL, origURL, redirectURL) } + if strings.ContainsAny(redirectURL, "\r\n") { + s := ctx.Response.String() + if strings.Contains(s, "\r\nInjected: yes\r\n") { + t.Fatalf("serialized response contains injected header line: %q", s) + } + } } func TestServerResponseServerHeader(t *testing.T) {