From 7945a5535e7125f4fc3cff7606028d41acab5223 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 2 Mar 2026 16:14:02 -0800 Subject: [PATCH 1/3] check reverseproxy head proto --- modules/httplib/url.go | 20 +++++++++++++++----- modules/httplib/url_test.go | 11 +++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 0133334b2c357..bff846386287e 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -46,20 +46,30 @@ func IsRelativeURL(s string) bool { func getRequestScheme(req *http.Request) string { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto - if s := req.Header.Get("X-Forwarded-Proto"); s != "" { + if s := normalizeForwardedProto(req.Header.Get("X-Forwarded-Proto")); s != "" { return s } - if s := req.Header.Get("X-Forwarded-Protocol"); s != "" { + if s := normalizeForwardedProto(req.Header.Get("X-Forwarded-Protocol")); s != "" { return s } - if s := req.Header.Get("X-Url-Scheme"); s != "" { + if s := normalizeForwardedProto(req.Header.Get("X-Url-Scheme")); s != "" { return s } if s := req.Header.Get("Front-End-Https"); s != "" { - return util.Iif(s == "on", "https", "http") + return util.Iif(strings.EqualFold(s, "on"), "https", "http") } if s := req.Header.Get("X-Forwarded-Ssl"); s != "" { - return util.Iif(s == "on", "https", "http") + return util.Iif(strings.EqualFold(s, "on"), "https", "http") + } + return "" +} + +func normalizeForwardedProto(value string) string { + value, _, _ = strings.Cut(value, ",") + value = strings.TrimSpace(value) + value = strings.ToLower(value) + if value == "http" || value == "https" { + return value } return "" } diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index 728c455cfba30..4733e8d5a79a1 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -47,6 +47,8 @@ func TestGuessCurrentHostURL(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() headersWithProto := http.Header{"X-Forwarded-Proto": {"https"}} + maliciousProtoHeaders := http.Header{"X-Forwarded-Proto": {"http://attacker.host/?trash="}} + listProtoHeaders := http.Header{"X-Forwarded-Proto": {"https, http"}} t.Run("Legacy", func(t *testing.T) { defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLLegacy)() @@ -60,6 +62,12 @@ func TestGuessCurrentHostURL(t *testing.T) { // if "X-Forwarded-Proto" exists, then use it and "Host" header ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: headersWithProto}) assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: maliciousProtoHeaders}) + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: listProtoHeaders}) + assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx)) }) t.Run("Auto", func(t *testing.T) { @@ -76,6 +84,9 @@ func TestGuessCurrentHostURL(t *testing.T) { ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: headersWithProto}) assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: maliciousProtoHeaders}) + assert.Equal(t, "http://req-host:3000", GuessCurrentHostURL(ctx)) }) t.Run("Never", func(t *testing.T) { From 7f765bcd441b59b2e4cabb7fb90407abc06bd110 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 3 Mar 2026 17:19:13 +0800 Subject: [PATCH 2/3] fix --- modules/httplib/url.go | 30 +++++++++++++++--------------- modules/httplib/url_test.go | 4 ---- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/modules/httplib/url.go b/modules/httplib/url.go index bff846386287e..70cadf994bf12 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -46,32 +46,32 @@ func IsRelativeURL(s string) bool { func getRequestScheme(req *http.Request) string { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto - if s := normalizeForwardedProto(req.Header.Get("X-Forwarded-Proto")); s != "" { - return s + if proto, ok := parseForwardedProtoValue(req.Header.Get("X-Forwarded-Proto")); ok { + return proto } - if s := normalizeForwardedProto(req.Header.Get("X-Forwarded-Protocol")); s != "" { - return s + if proto, ok := parseForwardedProtoValue(req.Header.Get("X-Forwarded-Protocol")); ok { + return proto } - if s := normalizeForwardedProto(req.Header.Get("X-Url-Scheme")); s != "" { - return s + if proto, ok := parseForwardedProtoValue(req.Header.Get("X-Url-Scheme")); ok { + return proto } if s := req.Header.Get("Front-End-Https"); s != "" { - return util.Iif(strings.EqualFold(s, "on"), "https", "http") + return util.Iif(util.AsciiEqualFold(s, "on"), "https", "http") } if s := req.Header.Get("X-Forwarded-Ssl"); s != "" { - return util.Iif(strings.EqualFold(s, "on"), "https", "http") + return util.Iif(util.AsciiEqualFold(s, "on"), "https", "http") } return "" } -func normalizeForwardedProto(value string) string { - value, _, _ = strings.Cut(value, ",") - value = strings.TrimSpace(value) - value = strings.ToLower(value) - if value == "http" || value == "https" { - return value +func parseForwardedProtoValue(val string) (string, bool) { + if val != "" { + lower := strings.ToLower(val) + if lower == "http" || lower == "https" { + return lower, true + } } - return "" + return "", false } // GuessCurrentAppURL tries to guess the current full public URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index 4733e8d5a79a1..373693693b5ef 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -48,7 +48,6 @@ func TestGuessCurrentHostURL(t *testing.T) { defer test.MockVariableValue(&setting.AppSubURL, "/sub")() headersWithProto := http.Header{"X-Forwarded-Proto": {"https"}} maliciousProtoHeaders := http.Header{"X-Forwarded-Proto": {"http://attacker.host/?trash="}} - listProtoHeaders := http.Header{"X-Forwarded-Proto": {"https, http"}} t.Run("Legacy", func(t *testing.T) { defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLLegacy)() @@ -65,9 +64,6 @@ func TestGuessCurrentHostURL(t *testing.T) { ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: maliciousProtoHeaders}) assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) - - ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: listProtoHeaders}) - assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx)) }) t.Run("Auto", func(t *testing.T) { From 4622bc6a02bacecbd3d4cc51fc985d21fadaa66d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 5 Mar 2026 23:57:51 +0800 Subject: [PATCH 3/3] standard doesn't say they are case-insenstive, don't do unnecessary things --- modules/httplib/url.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 70cadf994bf12..a689c9e98ee34 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -56,20 +56,17 @@ func getRequestScheme(req *http.Request) string { return proto } if s := req.Header.Get("Front-End-Https"); s != "" { - return util.Iif(util.AsciiEqualFold(s, "on"), "https", "http") + return util.Iif(s == "on", "https", "http") } if s := req.Header.Get("X-Forwarded-Ssl"); s != "" { - return util.Iif(util.AsciiEqualFold(s, "on"), "https", "http") + return util.Iif(s == "on", "https", "http") } return "" } func parseForwardedProtoValue(val string) (string, bool) { - if val != "" { - lower := strings.ToLower(val) - if lower == "http" || lower == "https" { - return lower, true - } + if val == "http" || val == "https" { + return val, true } return "", false }