Files
llmgw/internal/router/adminguard_test.go
T
Ilker ManapandClaude Opus 5.5 d167686cf1 Stop a browser page, or a model definition, from running code on the nodes
The admin listener has no login. Reaching it is meant to mean holding the
control plane, so it binds to loopback and an operator reaches it over an
ssh tunnel — and so does every page open in that operator's browser. The
API decoded JSON whatever the Content-Type said, so a page could POST a
text/plain model definition, no preflight needed. A model's profile env
reached an engine the agent runs as root, unfiltered: LD_PRELOAD in a
definition was code execution on every node. A page that rebound its own
name to 127.0.0.1 could also read pprof's heap, which holds API keys.

The admin listener now answers only when Host is an address or localhost,
refuses requests a browser marks as cross-site (Origin, Sec-Fetch-Site),
and takes changes only as application/json, which no page can send across
origins without a preflight nobody grants. ctl sends that on every change,
bodiless DELETEs included; curl and ctl are otherwise unaffected.

Profile env that changes what runs rather than how — the dynamic loader's
LD_*, PATH, Python's PYTHON* start-up variables, BASH_ENV and their like —
is refused when a model is defined, and dropped again by the engine adapter
before the process starts, whatever sent the profile. None of the live
catalog's models sets any.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 00:29:52 +02:00

58 lines
2.0 KiB
Go

package router
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestTheAdminListenerRefusesWhatABrowserPageCouldSend(t *testing.T) {
h := AdminGuard(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}))
do := func(method, host, ct string, hdr map[string]string) int {
r := httptest.NewRequest(method, "/api/v1/models", strings.NewReader("{}"))
r.Host = host
if ct != "" {
r.Header.Set("Content-Type", ct)
}
for k, v := range hdr {
r.Header.Set(k, v)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w.Code
}
// What ctl and curl send.
if got := do("POST", "127.0.0.1:9090", "application/json", nil); got != 200 {
t.Errorf("ctl's request was refused: %d", got)
}
if got := do("GET", "localhost:9090", "", nil); got != 200 {
t.Errorf("a GET through an ssh tunnel was refused: %d", got)
}
if got := do("DELETE", "[::1]:9090", "application/json", nil); got != 200 {
t.Errorf("IPv6 loopback refused: %d", got)
}
// A page posting a form, or text/plain, needs no preflight.
if got := do("POST", "127.0.0.1:9090", "text/plain", nil); got != http.StatusUnsupportedMediaType {
t.Errorf("text/plain POST accepted: %d", got)
}
if got := do("POST", "127.0.0.1:9090", "", nil); got != http.StatusUnsupportedMediaType {
t.Errorf("a bodiless POST accepted: %d", got)
}
// DNS rebinding: the page's own name, now resolving to 127.0.0.1.
if got := do("GET", "evil.example:9090", "", nil); got != http.StatusMisdirectedRequest {
t.Errorf("a rebound name was answered: %d", got)
}
// And anything a browser marks as coming from elsewhere.
if got := do("POST", "127.0.0.1:9090", "application/json",
map[string]string{"Sec-Fetch-Site": "cross-site"}); got != http.StatusForbidden {
t.Errorf("cross-site fetch accepted: %d", got)
}
if got := do("POST", "127.0.0.1:9090", "application/json",
map[string]string{"Origin": "https://evil.example"}); got != http.StatusForbidden {
t.Errorf("foreign Origin accepted: %d", got)
}
}