package cabana import ( "net/http" "strings" ) const ( // requestedWithHeader is the custom header the admin SPA sends on every // request. A cross-site form or navigation cannot set it, and a // cross-origin fetch that sets it needs a CORS preflight the admin API // never answers (D-19). requestedWithHeader = "X-Requested-With" requestedWithAjax = "XMLHttpRequest" ) // requireAjax refuses a state-changing admin request that is not // Bearer-authenticated and does not carry X-Requested-With: XMLHttpRequest. // It runs before the wrapped handler, so a refused request is never decoded, // never looks up a controller and never reaches the database. The response // uses the fixed D-10 code forbidden. func requireAjax(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !csrfSafe(r) { WriteError(w, http.StatusForbidden, "forbidden", msgForbidden) return } next(w, r) } } func csrfSafe(r *http.Request) bool { switch r.Method { case http.MethodGet, http.MethodHead, http.MethodOptions: return true } if bearerToken(r) != "" { return true } return isAjax(r) } func isAjax(r *http.Request) bool { return strings.TrimSpace(r.Header.Get(requestedWithHeader)) == requestedWithAjax }