35 lines
1.4 KiB
Go
35 lines
1.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||
|
|
|
||
|
|
package web
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json/v2"
|
||
|
|
"log/slog"
|
||
|
|
"net/http"
|
||
|
|
)
|
||
|
|
|
||
|
|
// CrossOrigin refuses a state-changing request that a browser sent from
|
||
|
|
// another origin, using the standard library's Fetch Metadata check:
|
||
|
|
// Sec-Fetch-Site when the browser sends it, and the Origin header against
|
||
|
|
// the Host header otherwise.
|
||
|
|
//
|
||
|
|
// It is the outer gate, and the admin's per-session CSRF token is the
|
||
|
|
// inner one, because the two cover different cases: this refuses a
|
||
|
|
// cross-site request before any handler runs, and the token also refuses
|
||
|
|
// a same-site request (another port on the same host) and a browser that
|
||
|
|
// sends neither header, which this check deliberately allows as a
|
||
|
|
// non-browser client.
|
||
|
|
func CrossOrigin() func(http.Handler) http.Handler {
|
||
|
|
protection := http.NewCrossOriginProtection()
|
||
|
|
protection.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
slog.Warn("web: refused a cross-origin request",
|
||
|
|
"method", r.Method, "path", r.URL.Path, "origin", r.Header.Get("Origin"))
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
w.WriteHeader(http.StatusForbidden)
|
||
|
|
_ = json.MarshalWrite(w, map[string]any{"error": "cross_origin"}, json.Deterministic(true))
|
||
|
|
}))
|
||
|
|
return protection.Handler
|
||
|
|
}
|