Files
gitea/routers/common/forwardedheaders_test.go
T
silverwind 5e4ed4d508 fix: correctly parse and validate forwarded client IPs and remove unmaintained chi-middleware/proxy (#39190)
Replace the unmaintained `chi-middleware/proxy` with an in-tree handler
that detects client IPs correctly behind common reverse proxies.

1. Handle repeated header lines and comma-separated proxy hops.
2. Accept and normalize IPs, including `ip:port`.
3. Keep the client port from the header value or `X-Real-Port` instead
of logging port 0.
4. Prefer the last `X-Real-IP` line.
5. Keep the proxy's address when the selected value is invalid.
6. Read `Forwarded` only when `X-Forwarded-For` is absent.

Docs PR: https://gitea.com/gitea/docs/pulls/560
Fixes: https://github.com/go-gitea/gitea/issues/35682
Fixes: https://github.com/go-gitea/gitea/issues/36814
Co-authored-by: bircni <bircni@icloud.com>
2026-10-23 09:25:04 +00:00

58 lines
3.6 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestForwardedHeadersHandler(t *testing.T) {
defaultProxies := []string{"127.0.0.0/8", "::1/128"}
cases := []struct {
name string
remoteAddr string
limit int
proxies []string
header http.Header
expected string
}{
{"chain shorter than limit", "127.0.0.1:1234", 3, defaultProxies, http.Header{"X-Forwarded-For": {"1.1.1.1, 2.2.2.2"}}, "1.1.1.1:0"},
{"repeated header without spaces", "127.0.0.1:1234", 2, defaultProxies, http.Header{"X-Forwarded-For": {"1.1.1.1,2.2.2.2", "3.3.3.3"}}, "2.2.2.2:0"},
{"non-ip entry", "127.0.0.1:1234", 1, defaultProxies, http.Header{"X-Forwarded-For": {"evil, <script>"}}, "127.0.0.1:1234"},
{"mapped ipv4", "[::ffff:127.0.0.1]:1234", 1, defaultProxies, http.Header{"X-Forwarded-For": {"::ffff:1.2.3.4"}}, "1.2.3.4:0"},
{"entry with port", "127.0.0.1:1234", 1, defaultProxies, http.Header{"X-Forwarded-For": {"[2001:db8::1]:443"}}, "[2001:db8::1]:443"},
{"zoned ipv6 entry", "[::1]:1234", 1, defaultProxies, http.Header{"X-Forwarded-For": {"2001:db8::1%eth0"}}, "[2001:db8::1]:0"},
{"last real ip and port win", "127.0.0.1:1234", 1, defaultProxies, http.Header{"X-Real-Ip": {"8.8.8.8", "9.9.9.9"}, "X-Real-Port": {"1", "4321"}, "X-Forwarded-For": {"1.1.1.1"}}, "9.9.9.9:4321"},
{"empty real ip falls through", "127.0.0.1:1234", 1, defaultProxies, http.Header{"X-Real-Ip": {""}, "X-Forwarded-For": {"1.1.1.1"}}, "1.1.1.1:0"},
{"forwarded ignored with x-forwarded-for", "127.0.0.1:1234", 1, defaultProxies, http.Header{"X-Forwarded-For": {"1.1.1.1"}, "Forwarded": {"for=6.6.6.6"}}, "1.1.1.1:0"},
{"forwarded hops skip quoted separators, empty elements and malformed prefix", "127.0.0.1:1234", 2, defaultProxies, http.Header{"Forwarded": {`for="x, For="[2001:db8::1]:4711";host="a,b;for=6.6.6.6"`, `, for=_hidden`}}, "[2001:db8::1]:4711"},
{"forwarded quoted pair and obfuscated port", "127.0.0.1:1234", 1, defaultProxies, http.Header{"Forwarded": {`for="192.0.2\.43:_port"`}}, "192.0.2.43:0"},
{"forwarded repeated for", "127.0.0.1:1234", 1, defaultProxies, http.Header{"Forwarded": {"for=1.1.1.1;for=2.2.2.2"}}, "127.0.0.1:1234"},
{"untrusted peer", "8.8.8.8:1234", 1, defaultProxies, http.Header{"X-Forwarded-For": {"1.1.1.1"}}, "8.8.8.8:1234"},
{"zoned peer", "[fe80::1%en1]:1234", 1, []string{"fe80::1%en0"}, http.Header{"X-Forwarded-For": {"1.1.1.1"}}, "[fe80::1%en1]:1234"},
{"bare ip proxy after invalid one", "10.0.0.1:1234", 1, []string{"invalid", "10.0.0.1"}, http.Header{"X-Forwarded-For": {"1.1.1.1"}}, "1.1.1.1:0"},
{"mapped cidr proxy", "10.0.0.1:1234", 1, []string{"::ffff:10.0.0.0/104"}, http.Header{"X-Forwarded-For": {"1.1.1.1"}}, "1.1.1.1:0"},
{"mapped cidr spanning native ipv6", "[::1]:1234", 1, []string{"::ffff:10.0.0.0/80"}, http.Header{"X-Forwarded-For": {"1.1.1.1"}}, "1.1.1.1:0"},
{"wildcard proxy with zoned peer", "[fe80::1%en0]:1234", 1, []string{"*"}, http.Header{"X-Forwarded-For": {"1.1.1.1"}}, "1.1.1.1:0"},
{"unix socket", "@", 1, defaultProxies, http.Header{"X-Forwarded-For": {"1.1.1.1"}}, "1.1.1.1:0"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var got string
handler := ForwardedHeadersHandler(c.limit, c.proxies)(http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
got = req.RemoteAddr
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.RemoteAddr = c.remoteAddr
req.Header = c.header
handler.ServeHTTP(httptest.NewRecorder(), req)
assert.Equal(t, c.expected, got)
})
}
}