Ctrl K
ComunidadGuías…

internal/server/admin_allowlist_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: Apache-2.0

package server

import (
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"testing"

	appdb "github.com/X4Applegate/caddyui/internal/db"
	"github.com/X4Applegate/caddyui/internal/models"
)

// v2.65.0 (issue #140): the admin IP allowlist used to compare r.RemoteAddr,
// which behind any reverse proxy is the proxy. Two symmetrical failures came
// out of that: allowlisting your real address locked you out, and the obvious
// workaround — allowlisting the proxy — admitted everyone who arrived through
// it while the UI still read as "restricted".

func allowlistTestServer(t *testing.T, allowlist string) *Server {
	t.Helper()
	conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = conn.Close() })
	if err := models.SetSetting(conn, settingAdminAllowlist, allowlist); err != nil {
		t.Fatal(err)
	}
	return &Server{DB: conn}
}

// gateStatus runs a request through adminIPGate and reports the status.
func gateStatus(t *testing.T, s *Server, remoteAddr string, headers map[string]string) int {
	t.Helper()
	r := httptest.NewRequest("GET", "/", nil)
	r.RemoteAddr = remoteAddr
	for k, v := range headers {
		r.Header.Set(k, v)
	}
	rec := httptest.NewRecorder()
	s.adminIPGate(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})).ServeHTTP(rec, r)
	return rec.Code
}

// The reporter's lockout: the visitor's real address is on the list, but the
// request reaches CaddyUI through a proxy.
func TestAllowlistedVisitorBehindAProxyGetsIn(t *testing.T) {
	s := allowlistTestServer(t, "203.0.113.9")
	// 127.0.0.1 is a trusted proxy by default (loopback/private), so the
	// forwarded address is believed.
	if got := gateStatus(t, s, "127.0.0.1:5555", map[string]string{"X-Forwarded-For": "203.0.113.9"}); got != http.StatusOK {
		t.Errorf("allowlisted visitor got %d, want 200 — this is the lockout from #140", got)
	}
}

func TestNonAllowlistedVisitorBehindAProxyIsRefused(t *testing.T) {
	s := allowlistTestServer(t, "203.0.113.9")
	if got := gateStatus(t, s, "127.0.0.1:5555", map[string]string{"X-Forwarded-For": "198.51.100.66"}); got != http.StatusForbidden {
		t.Errorf("unlisted visitor got %d, want 403", got)
	}
}

// The security half: a visitor cannot prepend an allowlisted address to
// X-Forwarded-For and be believed. Only the right-most entry — the one the
// trusted proxy appended — counts.
func TestForwardedForCannotBeSpoofed(t *testing.T) {
	s := allowlistTestServer(t, "203.0.113.9")
	if got := gateStatus(t, s, "127.0.0.1:5555", map[string]string{"X-Forwarded-For": "203.0.113.9, 198.51.100.66"}); got != http.StatusForbidden {
		t.Errorf("a spoofed left-most entry was believed (%d); only the right-most entry is the proxy's", got)
	}
}

// An untrusted peer's headers are ignored entirely, so a direct caller cannot
// claim an allowlisted address.
func TestUntrustedPeerHeadersAreIgnored(t *testing.T) {
	s := allowlistTestServer(t, "203.0.113.9")
	if got := gateStatus(t, s, "198.51.100.66:5555", map[string]string{"X-Forwarded-For": "203.0.113.9"}); got != http.StatusForbidden {
		t.Errorf("headers from an untrusted public peer were believed (%d)", got)
	}
}

func TestDirectConnectionFromAnAllowlistedAddressGetsIn(t *testing.T) {
	s := allowlistTestServer(t, "198.51.100.66")
	if got := gateStatus(t, s, "198.51.100.66:5555", nil); got != http.StatusOK {
		t.Errorf("direct allowlisted connection got %d, want 200", got)
	}
}

func TestEmptyAllowlistAllowsEveryone(t *testing.T) {
	s := allowlistTestServer(t, "")
	if got := gateStatus(t, s, "198.51.100.66:5555", nil); got != http.StatusOK {
		t.Errorf("an unset allowlist must not gate anything, got %d", got)
	}
}

func TestCIDREntriesMatch(t *testing.T) {
	s := allowlistTestServer(t, "10.0.0.0/8\n203.0.113.0/24")
	if got := gateStatus(t, s, "127.0.0.1:5555", map[string]string{"X-Forwarded-For": "203.0.113.77"}); got != http.StatusOK {
		t.Errorf("address inside an allowlisted CIDR got %d, want 200", got)
	}
	if got := gateStatus(t, s, "127.0.0.1:5555", map[string]string{"X-Forwarded-For": "203.0.114.1"}); got != http.StatusForbidden {
		t.Errorf("address outside every CIDR got %d, want 403", got)
	}
}

// The lockout escape hatch. The stored list must be left alone so removing
// the variable restores it.
func TestEnvOverrideReplacesTheStoredAllowlist(t *testing.T) {
	s := allowlistTestServer(t, "203.0.113.9")
	t.Setenv(envAdminAllowlist, "198.51.100.66")
	if got := s.adminAllowlist(); got != "198.51.100.66" {
		t.Errorf("effective allowlist = %q, want the environment value", got)
	}
	if !s.adminAllowlistFromEnv() {
		t.Error("the UI needs to know the stored list is not in force")
	}
	stored, _ := models.GetSetting(s.DB, settingAdminAllowlist)
	if stored != "203.0.113.9" {
		t.Errorf("stored setting was modified (%q); removing the variable must restore it", stored)
	}
}

func TestEnvOverrideOffDisablesTheGate(t *testing.T) {
	s := allowlistTestServer(t, "203.0.113.9")
	for _, v := range []string{"off", "OFF", "none", "any", "*"} {
		t.Setenv(envAdminAllowlist, v)
		if got := s.adminAllowlist(); got != "" {
			t.Errorf("%q should disable the gate, got %q", v, got)
		}
		if got := gateStatus(t, s, "198.51.100.66:5555", nil); got != http.StatusOK {
			t.Errorf("%q should let a locked-out admin back in, got %d", v, got)
		}
	}
}

// An empty variable is "not set", not "allow nobody" — otherwise a blank
// value in a compose file would be an accidental lockout of its own.
func TestEmptyEnvFallsBackToTheStoredList(t *testing.T) {
	s := allowlistTestServer(t, "203.0.113.9")
	t.Setenv(envAdminAllowlist, "")
	if got := s.adminAllowlist(); got != "203.0.113.9" {
		t.Errorf("effective allowlist = %q, want the stored value", got)
	}
	if s.adminAllowlistFromEnv() {
		t.Error("an empty variable is not an override")
	}
}