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>
58 lines
2.0 KiB
Go
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)
|
|
}
|
|
}
|