oficina / home Public

Cover shared origins and scope CSRF to credential handlers

Paul Campbell pushed by paul@keymail.dev 0ad7df09ef3d2ecf0e1afd7748e2e0af02b43c0b parent 4322001
2 files changed, +30 −7
  • internal/home/app.go +6 −7
  • internal/home/origins_test.go +24 −0
diff --git a/internal/home/app.go b/internal/home/app.go
index ef62fdd..bec990a 100644
--- a/internal/home/app.go
+++ b/internal/home/app.go
@@ -130,14 +130,7 @@ func New(secrets Secrets, d *db.DB, logger *slog.Logger) (*App, error) {
func (a *App) Router() http.Handler {
r := chi.NewRouter()
- // Router-wide, not just on the verify POST. POST /signin needs it
- // too: it is an unauthenticated endpoint that sends mail and makes
- // outbound DNS and HTTPS calls decided by the submitted address — a
- // blind-SSRF surface by design — and csrf.Protect is pure origin
- // evidence (Sec-Fetch-Site, then Origin, then Referer) that never
- // consults a session, so it works before anybody is signed in.
r.Use(serveKnownHost)
- r.Use(csrf.Protect(Origin))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
if r.Host == credentialHost {
@@ -160,6 +153,12 @@ func (a *App) Router() http.Handler {
r.Group(func(r chi.Router) {
r.Use(credentialOnly)
+ // POST /signin is an unauthenticated endpoint that sends mail and makes
+ // outbound DNS and HTTPS calls decided by the submitted address — a
+ // blind-SSRF surface by design — and csrf.Protect is pure origin
+ // evidence (Sec-Fetch-Site, then Origin, then Referer) that never
+ // consults a session, so it works before anybody is signed in.
+ r.Use(csrf.Protect(Origin))
r.Get("/signin", a.handleSigninForm)
r.Post("/signin", a.handleSigninBegin)
r.Get("/signin/sent", a.handleSigninSent)
diff --git a/internal/home/origins_test.go b/internal/home/origins_test.go
index 6392707..6438875 100644
--- a/internal/home/origins_test.go
+++ b/internal/home/origins_test.go
@@ -75,3 +75,27 @@ func TestDirectoryIsNotWithinCredentialRP(t *testing.T) {
t.Fatal("directory overlaps the credential origin or relying party")
}
}
+
+func TestSharedRoutesOnBothOrigins(t *testing.T) {
+ app := newTestApp(t)
+ for _, host := range []string{credentialHost, directoryHost} {
+ for _, path := range []string{"/healthz", "/static/tokens.css", "/static/theme.css"} {
+ req := httptest.NewRequest(http.MethodGet, "https://"+host+path, nil)
+ rec := httptest.NewRecorder()
+ app.Router().ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK || len(rec.Result().Cookies()) != 0 {
+ t.Errorf("host=%s path=%s status=%d cookies=%d", host, path, rec.Code, len(rec.Result().Cookies()))
+ }
+ }
+ }
+}
+
+func TestDirectoryCredentialPostRefusedBeforeCSRF(t *testing.T) {
+ app := newTestApp(t)
+ req := httptest.NewRequest(http.MethodPost, directoryOrigin+"/signin", nil)
+ rec := httptest.NewRecorder()
+ app.Router().ServeHTTP(rec, req)
+ if rec.Code != http.StatusNotFound || rec.Body.Len() != 0 {
+ t.Fatalf("directory credential POST status=%d body=%q", rec.Code, rec.Body.String())
+ }
+}