Files
weichenandCursor 8eda35f365 Refuse open listeners and cross-site writes without auth
Why: The embedded tier starts without auth, and nothing checked Origin or Host, so a public bind or a foreign page could call write APIs.
Evidence: go test ./internal/config ./internal/server covering listen policy, origin rejection, and response headers.
Impact: Non-loopback without auth exits before opening the database. HTML artifacts get a sandbox CSP. Reverse proxies must list their public host in allowedHosts.
Refs: feature/sillytavern-review
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 17:28:46 +00:00

82 lines
2.9 KiB
Go

package server
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func guard(auth bool, hosts ...string) RequestGuard {
return RequestGuard{
AuthOn: auth, AllowedHosts: hosts,
Next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}),
}
}
func TestRequestGuardWrites(t *testing.T) {
cases := []struct {
name string
auth bool
host string
origin string
site string
want int
}{
{"same origin", false, "127.0.0.1:8788", "http://127.0.0.1:8788", "", http.StatusNoContent},
{"cross origin", false, "127.0.0.1:8788", "https://evil.example", "", http.StatusForbidden},
{"null origin", false, "127.0.0.1:8788", "null", "", http.StatusForbidden},
{"sec-fetch cross", false, "127.0.0.1:8788", "", "cross-site", http.StatusForbidden},
{"sec-fetch same", false, "127.0.0.1:8788", "", "same-origin", http.StatusNoContent},
{"no browser headers", false, "127.0.0.1:8788", "", "", http.StatusNoContent},
{"allowed host origin", false, "127.0.0.1:8788", "https://app.example", "", http.StatusNoContent},
{"evil host without auth", false, "evil.example", "", "", http.StatusForbidden},
{"evil host with auth", true, "evil.example", "", "", http.StatusNoContent},
{"ip host", false, "192.168.1.9:8788", "", "", http.StatusNoContent},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/memory", strings.NewReader(`{}`))
req.Host = tc.host
if tc.origin != "" {
req.Header.Set("Origin", tc.origin)
}
if tc.site != "" {
req.Header.Set("Sec-Fetch-Site", tc.site)
}
rec := httptest.NewRecorder()
hosts := []string{}
if tc.name == "allowed host origin" {
hosts = []string{"app.example"}
}
guard(tc.auth, hosts...).ServeHTTP(rec, req)
if rec.Code != tc.want {
t.Fatalf("status %d want %d body %s", rec.Code, tc.want, rec.Body.String())
}
if rec.Code == http.StatusForbidden && !strings.Contains(rec.Body.String(), "allowedHosts") {
t.Fatalf("refusal should mention allowedHosts: %s", rec.Body.String())
}
})
}
}
func TestPageAndArtifactHeaders(t *testing.T) {
rec := httptest.NewRecorder()
setPageHeaders(rec)
if rec.Header().Get("Content-Security-Policy") == "" || rec.Header().Get("X-Content-Type-Options") != "nosniff" || rec.Header().Get("Referrer-Policy") != "same-origin" {
t.Fatalf("page headers: %v", rec.Header())
}
art := httptest.NewRecorder()
setArtifactHeaders(art, "text/html; charset=utf-8")
if !strings.Contains(art.Header().Get("Content-Security-Policy"), "sandbox") || art.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Fatalf("artifact headers: %v", art.Header())
}
plain := httptest.NewRecorder()
setArtifactHeaders(plain, "text/plain")
if plain.Header().Get("Content-Security-Policy") != "" {
t.Fatalf("plain artifact should not be sandboxed: %v", plain.Header())
}
}