fix: CSRF and WS origin check trust request Host header (same-origin)
This commit is contained in:
+34
-15
@@ -26,8 +26,11 @@ var upgrader = websocket.Upgrader{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetAllowedOrigins replaces the default upgrader with one that validates
|
// SetAllowedOrigins replaces the default upgrader with one that validates
|
||||||
// the Origin header against a trusted set. Call once at startup.
|
// the Origin header. Accepts if:
|
||||||
// Exact matches are checked first, then hostname-based matching is used.
|
// 1. Exact match in origins list, OR
|
||||||
|
// 2. Hostname matches a trusted hostname, OR
|
||||||
|
// 3. Hostname matches the request's own Host header (same-origin), OR
|
||||||
|
// 4. Localhost variant
|
||||||
func SetAllowedOrigins(origins []string) {
|
func SetAllowedOrigins(origins []string) {
|
||||||
originSet := make(map[string]struct{}, len(origins))
|
originSet := make(map[string]struct{}, len(origins))
|
||||||
hostSet := make(map[string]struct{}, len(origins))
|
hostSet := make(map[string]struct{}, len(origins))
|
||||||
@@ -53,34 +56,50 @@ func SetAllowedOrigins(origins []string) {
|
|||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
return true // non-browser clients
|
return true
|
||||||
}
|
}
|
||||||
if _, ok := originSet[origin]; ok {
|
if _, ok := originSet[origin]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
oh := origin
|
oh := extractHostname(origin)
|
||||||
if idx := strings.Index(oh, "://"); idx >= 0 {
|
|
||||||
oh = oh[idx+3:]
|
// Match trusted hostnames
|
||||||
}
|
|
||||||
if idx := strings.Index(oh, "/"); idx >= 0 {
|
|
||||||
oh = oh[:idx]
|
|
||||||
}
|
|
||||||
if idx := strings.LastIndex(oh, ":"); idx >= 0 {
|
|
||||||
oh = oh[:idx]
|
|
||||||
}
|
|
||||||
oh = strings.TrimPrefix(oh, "[")
|
|
||||||
oh = strings.TrimSuffix(oh, "]")
|
|
||||||
if _, ok := hostSet[oh]; ok {
|
if _, ok := hostSet[oh]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match request's own Host header (same-origin)
|
||||||
|
reqHost := extractHostname(r.Host)
|
||||||
|
if reqHost != "" && oh == reqHost {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Localhost variants
|
||||||
if oh == "localhost" || oh == "127.0.0.1" || oh == "::1" {
|
if oh == "localhost" || oh == "127.0.0.1" || oh == "::1" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractHostname(s string) string {
|
||||||
|
h := s
|
||||||
|
if idx := strings.Index(h, "://"); idx >= 0 {
|
||||||
|
h = h[idx+3:]
|
||||||
|
}
|
||||||
|
if idx := strings.Index(h, "/"); idx >= 0 {
|
||||||
|
h = h[:idx]
|
||||||
|
}
|
||||||
|
if idx := strings.LastIndex(h, ":"); idx >= 0 {
|
||||||
|
h = h[:idx]
|
||||||
|
}
|
||||||
|
h = strings.TrimPrefix(h, "[")
|
||||||
|
h = strings.TrimSuffix(h, "]")
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
// Client is a middleman between the websocket connection and the hub.
|
// Client is a middleman between the websocket connection and the hub.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Hub *Hub
|
Hub *Hub
|
||||||
|
|||||||
+105
-108
@@ -1,108 +1,105 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSRFProtect returns middleware that validates the Origin header on
|
// CSRFProtect returns middleware that validates the Origin header on
|
||||||
// state-changing methods (POST, PUT, PATCH, DELETE). Requests from origins
|
// state-changing methods (POST, PUT, PATCH, DELETE).
|
||||||
// not in the trustedOrigins list are rejected with 403. Non-browser clients
|
//
|
||||||
// (curl, bots) that send no Origin or Referer header are allowed through,
|
// An origin is trusted if:
|
||||||
// since they are not subject to CSRF.
|
// 1. It's in the explicit additionalOrigins list, OR
|
||||||
//
|
// 2. Its hostname matches the configured host, OR
|
||||||
// If host is non-empty, any Origin whose hostname matches host is allowed.
|
// 3. Its hostname matches the request's own Host header (same-origin check)
|
||||||
// additionalOrigins is a list of extra fully-qualified origins to trust
|
func CSRFProtect(host, port string, additionalOrigins []string) func(http.Handler) http.Handler {
|
||||||
// (e.g. from environment variable DUMPSTER_CSRF_ORIGINS, comma-separated).
|
originSet := make(map[string]struct{})
|
||||||
func CSRFProtect(host, port string, additionalOrigins []string) func(http.Handler) http.Handler {
|
for _, o := range additionalOrigins {
|
||||||
// Build a set of exact-match origins for quick lookup
|
o = strings.TrimSpace(o)
|
||||||
originSet := make(map[string]struct{})
|
if o != "" {
|
||||||
for _, o := range additionalOrigins {
|
originSet[o] = struct{}{}
|
||||||
o = strings.TrimSpace(o)
|
}
|
||||||
if o != "" {
|
}
|
||||||
originSet[o] = struct{}{}
|
|
||||||
}
|
hostname := host
|
||||||
}
|
if idx := strings.LastIndex(hostname, ":"); idx > 0 {
|
||||||
|
hostname = hostname[:idx]
|
||||||
// Extract hostname from host (strip port if present)
|
}
|
||||||
hostname := host
|
|
||||||
if idx := strings.LastIndex(hostname, ":"); idx > 0 {
|
return func(next http.Handler) http.Handler {
|
||||||
hostname = hostname[:idx]
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
switch r.Method {
|
||||||
|
case "POST", "PUT", "PATCH", "DELETE":
|
||||||
return func(next http.Handler) http.Handler {
|
origin := r.Header.Get("Origin")
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
if origin == "" {
|
||||||
switch r.Method {
|
referer := r.Header.Get("Referer")
|
||||||
case "POST", "PUT", "PATCH", "DELETE":
|
if referer != "" {
|
||||||
origin := r.Header.Get("Origin")
|
for o := range originSet {
|
||||||
if origin == "" {
|
if strings.HasPrefix(referer, o) {
|
||||||
// Fallback: extract origin from Referer
|
origin = o
|
||||||
referer := r.Header.Get("Referer")
|
break
|
||||||
if referer != "" {
|
}
|
||||||
for o := range originSet {
|
}
|
||||||
if strings.HasPrefix(referer, o) {
|
}
|
||||||
origin = o
|
}
|
||||||
break
|
|
||||||
}
|
if origin != "" {
|
||||||
}
|
// Extract request host for same-origin check
|
||||||
}
|
reqHost := r.Host
|
||||||
}
|
if idx := strings.LastIndex(reqHost, ":"); idx > 0 {
|
||||||
|
reqHost = reqHost[:idx]
|
||||||
if origin != "" {
|
}
|
||||||
if !isOriginTrusted(origin, hostname, port, originSet) {
|
|
||||||
http.Error(w, `{"error":"forbidden origin"}`, http.StatusForbidden)
|
if !isOriginTrusted(origin, hostname, port, reqHost, originSet) {
|
||||||
return
|
http.Error(w, `{"error":"forbidden origin"}`, http.StatusForbidden)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
// If both Origin and Referer are empty, allow through
|
}
|
||||||
// (non-browser client like curl, bots, mobile apps)
|
}
|
||||||
}
|
next.ServeHTTP(w, r)
|
||||||
next.ServeHTTP(w, r)
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
func isOriginTrusted(origin, host, port, reqHost string, originSet map[string]struct{}) bool {
|
||||||
// isOriginTrusted checks if the given Origin header value is trusted.
|
if _, ok := originSet[origin]; ok {
|
||||||
// An origin is trusted if:
|
return true
|
||||||
// 1. It's in the explicit originSet, OR
|
}
|
||||||
// 2. Its hostname matches the configured host (any scheme/port)
|
|
||||||
func isOriginTrusted(origin, host, port string, originSet map[string]struct{}) bool {
|
withoutScheme := origin
|
||||||
if _, ok := originSet[origin]; ok {
|
if idx := strings.Index(withoutScheme, "://"); idx >= 0 {
|
||||||
return true
|
withoutScheme = withoutScheme[idx+3:]
|
||||||
}
|
}
|
||||||
|
if idx := strings.Index(withoutScheme, "/"); idx >= 0 {
|
||||||
// Parse the origin to extract hostname and port
|
withoutScheme = withoutScheme[:idx]
|
||||||
// Origin format: scheme://host[:port]
|
}
|
||||||
withoutScheme := origin
|
|
||||||
if idx := strings.Index(withoutScheme, "://"); idx >= 0 {
|
originHost := withoutScheme
|
||||||
withoutScheme = withoutScheme[idx+3:]
|
originPort := ""
|
||||||
}
|
if idx := strings.LastIndex(originHost, ":"); idx >= 0 {
|
||||||
// Remove path if any
|
originPort = originHost[idx+1:]
|
||||||
if idx := strings.Index(withoutScheme, "/"); idx >= 0 {
|
originHost = originHost[:idx]
|
||||||
withoutScheme = withoutScheme[:idx]
|
}
|
||||||
}
|
|
||||||
|
originHost = strings.TrimPrefix(originHost, "[")
|
||||||
originHost := withoutScheme
|
originHost = strings.TrimSuffix(originHost, "]")
|
||||||
originPort := ""
|
|
||||||
if idx := strings.LastIndex(originHost, ":"); idx >= 0 {
|
// Match configured host
|
||||||
originPort = originHost[idx+1:]
|
if host != "" && originHost == host {
|
||||||
originHost = originHost[:idx]
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip brackets from IPv6
|
// Match request's own Host header (same-origin)
|
||||||
originHost = strings.TrimPrefix(originHost, "[")
|
if reqHost != "" && originHost == reqHost {
|
||||||
originHost = strings.TrimSuffix(originHost, "]")
|
return true
|
||||||
|
}
|
||||||
if host != "" && originHost == host {
|
|
||||||
return true
|
// Localhost variants
|
||||||
}
|
if originHost == "localhost" || originHost == "127.0.0.1" || originHost == "::1" {
|
||||||
|
if port == "" || originPort == "" || originPort == port {
|
||||||
// Also trust localhost variants
|
return true
|
||||||
if originHost == "localhost" || originHost == "127.0.0.1" || originHost == "::1" {
|
}
|
||||||
if port == "" || originPort == "" || originPort == port {
|
}
|
||||||
return true
|
|
||||||
}
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user