The session cookie is scoped to the parent domain and is SameSite=Lax, and every *.parodia.dev host is the same site as Pete as far as Lax is concerned. So a form on any sibling subdomain could post a bet, a mischief order or an equip change and the browser would attach whoever's signed-in cookie to it. Nothing on the server looked at where a request came from. The whole handler now sits behind http.NewCrossOriginProtection. It reads Sec-Fetch-Site (falling back to Origin against Host) and refuses a state-changing request from any other origin with a 403. GETs are untouched, and so is anything that carries neither header, which is exactly what gogobee's bearer-token calls from Go look like.
52 lines
1.6 KiB
Go
52 lines
1.6 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"pete/internal/config"
|
|
"pete/internal/storage"
|
|
)
|
|
|
|
// A sibling parodia.dev host is the same *site* as Pete, so SameSite=Lax lets
|
|
// the session cookie ride along on its forms. The server has to refuse those
|
|
// itself, and must still let a same-origin page and a headerless server-to-server
|
|
// call (gogobee, with its bearer token) through.
|
|
func TestCrossOriginWritesAreRefused(t *testing.T) {
|
|
storage.Close()
|
|
if err := storage.Init(filepath.Join(t.TempDir(), "csrf.db")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { storage.Close() })
|
|
|
|
s, err := New(config.WebConfig{SiteTitle: "Pete", ListenAddr: ":0"}, nil, true, config.AdventureConfig{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
post := func(hdr map[string]string) int {
|
|
req := httptest.NewRequest("POST", "https://news.parodia.dev/api/state/read", nil)
|
|
for k, v := range hdr {
|
|
req.Header.Set(k, v)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
s.srv.Handler.ServeHTTP(rec, req)
|
|
return rec.Code
|
|
}
|
|
|
|
if code := post(map[string]string{"Sec-Fetch-Site": "cross-site"}); code != http.StatusForbidden {
|
|
t.Errorf("cross-site POST = %d, want 403", code)
|
|
}
|
|
if code := post(map[string]string{"Sec-Fetch-Site": "same-site", "Origin": "https://evil.parodia.dev"}); code != http.StatusForbidden {
|
|
t.Errorf("sibling-subdomain POST = %d, want 403", code)
|
|
}
|
|
if code := post(map[string]string{"Sec-Fetch-Site": "same-origin"}); code == http.StatusForbidden {
|
|
t.Error("same-origin POST was refused")
|
|
}
|
|
if code := post(nil); code == http.StatusForbidden {
|
|
t.Error("headerless server-to-server POST was refused")
|
|
}
|
|
}
|