Files
pulse/internal/api/method_guard.go
T
rcourtman 2ce3605454 Refuse GET and HEAD on discovery settings and SAML logout
Two routes registered without a mux method pattern changed state on any
method. The demo-mode read-only guard and the CSRF check both admit GET and
HEAD as reads, and the SameSite=Lax session cookie rides cross-site
top-level GET navigations, so each was writable by a safe method:

- /api/discovery/settings had no method check, so a GET or HEAD carrying a
  JSON body changed the discovery staleness threshold, including on a
  DEMO_MODE instance.
- SP-initiated SAML logout at /api/saml/{id}/logout cleared the session and
  redirected to the IdP on GET. Nothing in the UI calls it, the /api/logout
  fallback it delegates to is already POST-only, and the IdP's
  LogoutResponse returns to /slo, not here.

Add requireRequestMethod (405 with Allow) as the shared handler-level
guard. Discovery settings keeps PUT, its documented method, and POST, which
it accepted before; SAML logout takes POST only. An audit of every route in
internal/api found no other caller-requested write reachable by GET or HEAD
beyond the Assistant session routes guarded in #2670. GET flows the
protocol requires (OIDC login and callback, SAML login initiation and SLO,
magic-link and cloud handoff, checkout bridges) and reads that run lazy
initialization or idempotent reconciliation are left as they are. One
known exception is deferred: single-resource report generation still runs
on GET and can call the configured AI narrator, which records provider
usage in the cost ledger; moving it behind POST changes the report API the
UI downloads from, so it is tracked separately.
2026-10-07 19:31:50 +01:00

24 lines
788 B
Go

package api
import (
"net/http"
"strings"
)
// requireRequestMethod admits only the listed methods to a handler that
// changes state, answering anything else with 405 and an Allow header. Bare
// mux patterns and path-dispatched routes do not filter methods, and GET/HEAD
// pass both the demo-mode read-only guard and the CSRF check while
// SameSite=Lax session cookies ride cross-site top-level GET navigations, so a
// mutating handler must refuse safe methods itself.
func requireRequestMethod(w http.ResponseWriter, r *http.Request, allowed ...string) bool {
for _, method := range allowed {
if r.Method == method {
return true
}
}
w.Header().Set("Allow", strings.Join(allowed, ", "))
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return false
}