Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
95 lines
3.4 KiB
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)
|
|
}
|
|
}
|