Files
nmon/internal/auth/proxy_test.go

95 lines
3.4 KiB
Go

package auth_test
import (
"net/http"
"net/http/httptest"
"net/netip"
"testing"
"github.com/v10networks/nmon/internal/auth"
)
func resolve(t *testing.T, cidrs []string, peer string, hdr map[string]string, tls bool) auth.Client {
t.Helper()
p, err := auth.ParseProxies(cidrs)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodGet, "http://nmon.internal/", nil)
r.RemoteAddr = peer
for k, v := range hdr {
r.Header.Set(k, v)
}
if tls {
r.TLS = &tlsState
}
return p.Resolve(r)
}
func TestForwardedHeadersAreHonouredOnlyFromTheTrustedProxy(t *testing.T) {
hdr := map[string]string{"X-Forwarded-For": "203.0.113.9", "X-Forwarded-Proto": "https", "X-Forwarded-Host": "nmon.example.com"}
proxies := []string{"10.0.0.0/8"}
c := resolve(t, proxies, "10.1.2.3:4444", hdr, false)
if c.IP != netip.MustParseAddr("203.0.113.9") || c.Scheme != "https" || c.Host != "nmon.example.com" || !c.ViaProxy {
t.Fatalf("trusted proxy: %+v", c)
}
c = resolve(t, proxies, "192.0.2.50:4444", hdr, false)
if c.IP != netip.MustParseAddr("192.0.2.50") || c.Scheme != "http" || c.Host != "nmon.internal" || c.ViaProxy {
t.Fatalf("untrusted peer must be ignored: %+v", c)
}
c = resolve(t, nil, "10.1.2.3:4444", hdr, false)
if c.ViaProxy || c.IP != netip.MustParseAddr("10.1.2.3") {
t.Fatalf("no proxies configured must trust nobody: %+v", c)
}
}
func TestForwardedForTakesTheNearestUntrustedHop(t *testing.T) {
// client -> edge (192.0.2.1, trusted) -> inner (10.0.0.5, trusted) -> nmon; the client lied about its own entry.
c := resolve(t, []string{"10.0.0.0/8", "192.0.2.1"}, "10.0.0.5:1",
map[string]string{"X-Forwarded-For": "1.1.1.1, 203.0.113.9, 192.0.2.1"}, false)
if c.IP != netip.MustParseAddr("203.0.113.9") {
t.Fatalf("client = %v, want 203.0.113.9 (a spoofed left-hand entry must not win)", c.IP)
}
}
func TestMalformedForwardedForFallsBackToThePeerOfTheLastGoodHop(t *testing.T) {
c := resolve(t, []string{"10.0.0.0/8"}, "10.0.0.5:1", map[string]string{"X-Forwarded-For": "garbage"}, false)
if c.IP != netip.MustParseAddr("10.0.0.5") {
t.Fatalf("got %v", c.IP)
}
}
func TestDirectTLSIsHTTPSAndBadProxyConfigIsRejected(t *testing.T) {
if c := resolve(t, nil, "192.0.2.50:1", nil, true); c.Scheme != "https" {
t.Fatalf("scheme %q", c.Scheme)
}
if _, err := auth.ParseProxies([]string{"10.0.0.0/33"}); err == nil {
t.Fatal("bad CIDR accepted")
}
if _, err := auth.ParseProxies([]string{"::1", "192.0.2.7"}); err != nil {
t.Fatalf("single addresses: %v", err)
}
}
func TestMiddlewareScrubsForwardedHeadersFromUntrustedPeers(t *testing.T) {
e := newEnv(t, envOpts{noOIDC: true, proxies: []string{"10.0.0.0/8"}})
_, secret := e.mintToken(auth.RoleViewer, 0)
hdr := bearer(secret)
hdr["X-Forwarded-For"] = "203.0.113.9"
_, body := call(t, http.DefaultClient, http.MethodGet, e.srv.URL+"/api/v1/whoami", hdr, "")
if want := `"xff":""`; !contains(body, want) || contains(body, "203.0.113.9") {
t.Fatalf("untrusted X-Forwarded-For reached the handler: %s", body)
}
}
func TestBreakGlassLockoutIgnoresSpoofedForwardedFor(t *testing.T) {
e := newEnv(t, envOpts{proxies: []string{"10.0.0.0/8"}}) // the test client is 127.0.0.1: not a proxy
for i := 0; i < 5; i++ {
bgLogin(t, e, "198.51.100."+string(rune('1'+i)), "wrong") // rotating the claimed address
}
if code := bgLogin(t, e, "198.51.100.9", bgPassword); code != http.StatusTooManyRequests {
t.Fatalf("spoofing X-Forwarded-For must not evade the lock: %d", code)
}
}