web: refuse writes that come from somebody else's page
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.
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -353,9 +353,16 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every state-changing request from a browser must come from this site's
|
||||||
|
// own origin. The session cookie is scoped to the parent domain and is
|
||||||
|
// SameSite=Lax, and every *.parodia.dev host counts as the same site, so
|
||||||
|
// without this a form on any sibling subdomain could bet, order or equip as
|
||||||
|
// whoever is signed in. It keys off Sec-Fetch-Site (falling back to Origin
|
||||||
|
// vs Host) and passes requests carrying neither, so gogobee's bearer-token
|
||||||
|
// calls from Go are untouched. GET/HEAD/OPTIONS are never blocked.
|
||||||
s.srv = &http.Server{
|
s.srv = &http.Server{
|
||||||
Addr: cfg.ListenAddr,
|
Addr: cfg.ListenAddr,
|
||||||
Handler: s.hostRouter(mux),
|
Handler: http.NewCrossOriginProtection().Handler(s.hostRouter(mux)),
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user