75 lines
2.7 KiB
Go
75 lines
2.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// Browser-driven cross-site attacks are the realistic threat here. The
|
|
// dashboard is reached with a browser session, so any page the operator
|
|
// happens to visit can make their browser issue requests to the pool
|
|
// with whatever credentials that session carries. Two shapes matter:
|
|
//
|
|
// - Cross-Site WebSocket Hijacking. The WebSocket handshake is not
|
|
// subject to the same-origin policy, so without a check any site
|
|
// could open /api/ws and read the whole snapshot: payout addresses,
|
|
// per-worker hashrates, block history.
|
|
// - CSRF against the state-changing POSTs. The admin endpoints take no
|
|
// body, so a plain <form> submission reaches them with no preflight
|
|
// to stop it, which is enough to zero the latency counters or kick
|
|
// off a rescan of a multi-million-line log.
|
|
//
|
|
// Both are closed by checking Origin, which browsers attach to every
|
|
// WebSocket handshake and to cross-site POSTs, and which page script
|
|
// cannot forge.
|
|
|
|
// originAllowed reports whether a request may proceed. A request with no
|
|
// Origin header is not browser-driven (curl, the StartOS actions, a
|
|
// health probe) and is allowed: an attacker able to make arbitrary
|
|
// direct requests already has network access and does not need the
|
|
// operator's browser to carry them.
|
|
func originAllowed(r *http.Request) bool {
|
|
origin := r.Header.Get("Origin")
|
|
if origin == "" {
|
|
return true
|
|
}
|
|
u, err := url.Parse(origin)
|
|
if err != nil || u.Host == "" {
|
|
return false
|
|
}
|
|
return strings.EqualFold(u.Host, r.Host)
|
|
}
|
|
|
|
// sameOrigin wraps a handler so cross-origin browser requests are
|
|
// refused. Applied to every state-changing route and to the WebSocket.
|
|
func (s *Server) sameOrigin(h http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !originAllowed(r) {
|
|
s.Log.Warn("refused cross-origin request",
|
|
"path", r.URL.Path, "origin", r.Header.Get("Origin"), "host", r.Host)
|
|
writeJSON(w, http.StatusForbidden, map[string]any{
|
|
"error": "cross-origin request refused",
|
|
})
|
|
return
|
|
}
|
|
h(w, r)
|
|
}
|
|
}
|
|
|
|
// securityHeaders sets the response headers that cost nothing and close
|
|
// off framing and MIME-sniffing. No CSP: the dashboard is a bundled
|
|
// Svelte app with inline styles, and a policy loose enough to run it
|
|
// would not be worth the claim of having one.
|
|
func securityHeaders(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
head := w.Header()
|
|
head.Set("X-Content-Type-Options", "nosniff")
|
|
// The dashboard has nothing to gain from being framed, and
|
|
// framing it invites clickjacking of the admin controls.
|
|
head.Set("X-Frame-Options", "DENY")
|
|
head.Set("Referrer-Policy", "no-referrer")
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|