246 lines
8.2 KiB
Go
246 lines
8.2 KiB
Go
package ip
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"reflect"
|
|
"testing"
|
|
|
|
ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestGetRealClientIPParser(t *testing.T) {
|
|
forwardedForType := reflect.TypeOf((*xForwardedForClientIPParser)(nil))
|
|
|
|
tests := []struct {
|
|
header string
|
|
errString string
|
|
parserType reflect.Type
|
|
}{
|
|
{"X-Forwarded-For", "", forwardedForType},
|
|
{"X-REAL-IP", "", forwardedForType},
|
|
{"x-proxyuser-ip", "", forwardedForType},
|
|
{"x-envoy-external-address", "", forwardedForType},
|
|
{"cf-connecting-ip", "", forwardedForType},
|
|
{"", "the http header key () is either invalid or unsupported", nil},
|
|
{"Forwarded", "the http header key (Forwarded) is either invalid or unsupported", nil},
|
|
{"2#* @##$$:kd", "the http header key (2#* @##$$:kd) is either invalid or unsupported", nil},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
p, err := GetRealClientIPParser(test.header)
|
|
|
|
if test.errString == "" {
|
|
assert.Nil(t, err)
|
|
} else {
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, test.errString, err.Error())
|
|
}
|
|
|
|
if test.parserType == nil {
|
|
assert.Nil(t, p)
|
|
} else {
|
|
assert.NotNil(t, p)
|
|
assert.Equal(t, test.parserType, reflect.TypeOf(p))
|
|
}
|
|
|
|
if xp, ok := p.(*xForwardedForClientIPParser); ok {
|
|
assert.Equal(t, http.CanonicalHeaderKey(test.header), xp.header)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestXForwardedForClientIPParser(t *testing.T) {
|
|
p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
|
|
|
|
tests := []struct {
|
|
name string
|
|
headerValue string
|
|
remoteAddr net.IP
|
|
trusted *NetSet
|
|
errString string
|
|
expectedIP net.IP
|
|
}{
|
|
// No trusted-proxy restriction configured (nil): behaves exactly like the historical
|
|
// leftmost-picking parser, regardless of remoteAddr.
|
|
{name: "empty header", headerValue: "", expectedIP: nil},
|
|
{name: "single hop", headerValue: "1.2.3.4", expectedIP: net.ParseIP("1.2.3.4")},
|
|
{name: "single hop v6", headerValue: "10::23", expectedIP: net.ParseIP("10::23")},
|
|
{name: "loopback v6", headerValue: "::1", expectedIP: net.ParseIP("::1")},
|
|
{name: "v6 with port", headerValue: "[::1]:1234", expectedIP: net.ParseIP("::1")},
|
|
{name: "v4 with port", headerValue: "10.0.10.11:1234", expectedIP: net.ParseIP("10.0.10.11")},
|
|
{name: "no trusted proxies falls back to leftmost", headerValue: "192.168.10.50, 10.0.0.1, 1.2.3.4", expectedIP: net.ParseIP("192.168.10.50")},
|
|
{name: "unparseable hop", headerValue: "nil", errString: "unable to parse ip (nil) from X-Forwarded-For header"},
|
|
{name: "malformed hop", headerValue: "10000.10000.10000.10000", errString: "unable to parse ip (10000.10000.10000.10000) from X-Forwarded-For header"},
|
|
|
|
// The reported attack: a client sends X-Forwarded-For set to an IP it wants to
|
|
// impersonate; the trusted reverse proxy appends the real client IP rather than
|
|
// replacing the header. Only the proxy's own /32 is trusted, so the walk must stop at
|
|
// the rightmost (attacker) hop instead of trusting the client-supplied leftmost value.
|
|
{
|
|
name: "spoofed leftmost hop behind a trusted proxy",
|
|
headerValue: "9.9.9.9, 6.6.6.6",
|
|
remoteAddr: net.ParseIP("10.0.0.5"),
|
|
trusted: mustNetSet(t, "10.0.0.5/32"),
|
|
expectedIP: net.ParseIP("6.6.6.6"),
|
|
},
|
|
// Direct peer isn't a trusted proxy at all: the header must be ignored entirely and
|
|
// the real connecting peer used, since nothing in the header can be believed.
|
|
{
|
|
name: "untrusted direct peer ignores header",
|
|
headerValue: "9.9.9.9",
|
|
remoteAddr: net.ParseIP("6.6.6.6"),
|
|
trusted: mustNetSet(t, "10.0.0.5/32"),
|
|
expectedIP: net.ParseIP("6.6.6.6"),
|
|
},
|
|
// A chain of two trusted proxies (client -> ProxyA(10.0.0.6) -> ProxyB(10.0.0.5) ->
|
|
// us) still resolves to the real (untrusted) client hop, skipping past both trusted
|
|
// proxy-appended hops and the attacker's forged leftmost claim.
|
|
{
|
|
name: "chain of trusted proxies",
|
|
headerValue: "9.9.9.9, 6.6.6.6, 10.0.0.6",
|
|
remoteAddr: net.ParseIP("10.0.0.5"),
|
|
trusted: mustNetSet(t, "10.0.0.5/32", "10.0.0.6/32"),
|
|
expectedIP: net.ParseIP("6.6.6.6"),
|
|
},
|
|
// Degenerate case: every hop (and the direct peer) is itself a trusted proxy; there's
|
|
// no untrusted hop to find, so fall back to the oldest (leftmost) entry.
|
|
{
|
|
name: "all hops trusted falls back to leftmost",
|
|
headerValue: "10.0.0.7, 10.0.0.6",
|
|
remoteAddr: net.ParseIP("10.0.0.5"),
|
|
trusted: mustNetSet(t, "10.0.0.5/32", "10.0.0.6/32", "10.0.0.7/32"),
|
|
expectedIP: net.ParseIP("10.0.0.7"),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
h := http.Header{}
|
|
h.Add("X-Forwarded-For", test.headerValue)
|
|
|
|
var trusted ipapi.TrustedProxies
|
|
if test.trusted != nil {
|
|
trusted = test.trusted
|
|
}
|
|
|
|
ip, err := p.GetRealClientIP(h, test.remoteAddr, trusted)
|
|
|
|
if test.errString == "" {
|
|
assert.Nil(t, err)
|
|
} else {
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, test.errString, err.Error())
|
|
}
|
|
|
|
if test.expectedIP == nil {
|
|
assert.Nil(t, ip)
|
|
} else {
|
|
assert.NotNil(t, ip)
|
|
assert.Equal(t, test.expectedIP, ip)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestXForwardedForClientIPParserIgnoresOthers(t *testing.T) {
|
|
p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
|
|
|
|
h := http.Header{}
|
|
expectedIPString := "192.168.10.50"
|
|
h.Add("X-Real-IP", "10.0.0.1")
|
|
h.Add("X-ProxyUser-IP", "10.0.0.1")
|
|
h.Add("X-Forwarded-For", expectedIPString)
|
|
ip, err := p.GetRealClientIP(h, nil, nil)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, ip)
|
|
assert.Equal(t, ip, net.ParseIP(expectedIPString))
|
|
}
|
|
|
|
// mustNetSet builds a *NetSet from CIDR/IP strings, failing the test on error.
|
|
func mustNetSet(t *testing.T, ipStrs ...string) *NetSet {
|
|
t.Helper()
|
|
set, err := ParseNetSet(ipStrs)
|
|
if err != nil {
|
|
t.Fatalf("failed to build NetSet: %v", err)
|
|
}
|
|
return set
|
|
}
|
|
|
|
func TestGetRemoteIP(t *testing.T) {
|
|
tests := []struct {
|
|
remoteAddr string
|
|
errString string
|
|
expectedIP net.IP
|
|
}{
|
|
// Unix domain sockets set RemoteAddr to "@"
|
|
{"@", "", nil},
|
|
{"", "unable to get ip and port from http.RemoteAddr ()", nil},
|
|
{"nil", "unable to get ip and port from http.RemoteAddr (nil)", nil},
|
|
{"235.28.129.186", "unable to get ip and port from http.RemoteAddr (235.28.129.186)", nil},
|
|
{"90::45", "unable to get ip and port from http.RemoteAddr (90::45)", nil},
|
|
{"192.168.73.165:14976, 10.4.201.15:18453", "unable to get ip and port from http.RemoteAddr (192.168.73.165:14976, 10.4.201.15:18453)", nil},
|
|
{"10000.10000.10000.10000:8080", "unable to parse ip (10000.10000.10000.10000)", nil},
|
|
{"[::1]:48290", "", net.ParseIP("::1")},
|
|
{"10.254.244.165:62750", "", net.ParseIP("10.254.244.165")},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
req := &http.Request{RemoteAddr: test.remoteAddr}
|
|
|
|
ip, err := getRemoteIP(req)
|
|
|
|
if test.errString == "" {
|
|
assert.Nil(t, err)
|
|
} else {
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, test.errString, err.Error())
|
|
}
|
|
|
|
if test.expectedIP == nil {
|
|
assert.Nil(t, ip)
|
|
} else {
|
|
assert.NotNil(t, ip)
|
|
assert.Equal(t, test.expectedIP, ip)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetClientString(t *testing.T) {
|
|
p := &xForwardedForClientIPParser{header: http.CanonicalHeaderKey("X-Forwarded-For")}
|
|
|
|
tests := []struct {
|
|
parser ipapi.RealClientIPParser
|
|
remoteAddr string
|
|
headerValue string
|
|
expectedClient string
|
|
expectedClientFull string
|
|
}{
|
|
// Should fail quietly, only printing warnings to the log
|
|
{nil, "", "", "", ""},
|
|
// Unix domain socket — no IP available
|
|
{nil, "@", "", "", ""},
|
|
{p, "127.0.0.1:11950", "", "127.0.0.1", "127.0.0.1"},
|
|
{p, "[::1]:28660", "99.103.56.12", "99.103.56.12", "::1 (99.103.56.12)"},
|
|
{nil, "10.254.244.165:62750", "", "10.254.244.165", "10.254.244.165"},
|
|
// Parser is nil, the contents of X-Forwarded-For should be ignored in all cases.
|
|
{nil, "[2001:470:26:307:a5a1:1177:2ae3:e9c3]:48290", "127.0.0.1", "2001:470:26:307:a5a1:1177:2ae3:e9c3", "2001:470:26:307:a5a1:1177:2ae3:e9c3"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
h := http.Header{}
|
|
h.Add("X-Forwarded-For", test.headerValue)
|
|
req := &http.Request{
|
|
Header: h,
|
|
RemoteAddr: test.remoteAddr,
|
|
}
|
|
|
|
client := GetClientString(test.parser, req, false, nil)
|
|
assert.Equal(t, test.expectedClient, client)
|
|
|
|
clientFull := GetClientString(test.parser, req, true, nil)
|
|
assert.Equal(t, test.expectedClientFull, clientFull)
|
|
}
|
|
}
|