Ver código fonte

feat(cmd/anubis): compute full XFF header (#328)

* feat(cmd/anubis): compute full XFF header

this one is pretty important to not pass
through blindly, as many applications and
frameworks will trust them

* feat(cmd/anubis): skip XFF compute if remote address is loopback

* docs: update CHANGELOG
Aurelia 2 meses atrás
pai
commit
4e2c9de708
3 arquivos alterados com 42 adições e 0 exclusões
  1. 1 0
      cmd/anubis/main.go
  2. 1 0
      docs/docs/CHANGELOG.md
  3. 40 0
      internal/headers.go

+ 1 - 0
cmd/anubis/main.go

@@ -280,6 +280,7 @@ func main() {
 	h = s
 	h = s
 	h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
 	h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
 	h = internal.XForwardedForToXRealIP(h)
 	h = internal.XForwardedForToXRealIP(h)
+	h = internal.XForwardedForUpdate(h)
 
 
 	srv := http.Server{Handler: h}
 	srv := http.Server{Handler: h}
 	listener, listenerUrl := setupListener(*bindNetwork, *bind)
 	listener, listenerUrl := setupListener(*bindNetwork, *bind)

+ 1 - 0
docs/docs/CHANGELOG.md

@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added documentation on how to use Anubis with Traefik in Docker
 - Added documentation on how to use Anubis with Traefik in Docker
 - Improved error handling in some edge cases
 - Improved error handling in some edge cases
 - Disable `generic-bot-catchall` rule because of its high false positive rate in real-world scenarios
 - Disable `generic-bot-catchall` rule because of its high false positive rate in real-world scenarios
+- Set or append to `X-Forwarded-For` header unless the remote connects over a loopback address [#328](https://github.com/TecharoHQ/anubis/issues/328)
 
 
 ## v1.16.0
 ## v1.16.0
 
 

+ 40 - 0
internal/headers.go

@@ -65,6 +65,46 @@ func XForwardedForToXRealIP(next http.Handler) http.Handler {
 	})
 	})
 }
 }
 
 
+// XForwardedForUpdate sets or updates the X-Forwarded-For header, adding
+// the known remote address to an existing chain if present
+func XForwardedForUpdate(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer next.ServeHTTP(w, r)
+
+		remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
+
+		if parsedRemoteIP := net.ParseIP(remoteIP); parsedRemoteIP != nil && parsedRemoteIP.IsLoopback() {
+			// anubis is likely deployed behind a local reverse proxy
+			// pass header as-is to not break existing applications
+			return
+		}
+
+		if err != nil {
+			slog.Warn("The default format of request.RemoteAddr should be IP:Port", "remoteAddr", r.RemoteAddr)
+			return
+		}
+		if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+			forwardedList := strings.Split(",", xff)
+			forwardedList = append(forwardedList, remoteIP)
+			// this behavior is equivalent to
+			// ingress-nginx "compute-full-forwarded-for"
+			// https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#compute-full-forwarded-for
+			//
+			// this would be the correct place to strip and/or flatten this list
+			//
+			// strip - iterate backwards and eliminate configured trusted IPs
+			// flatten - only return the last element to avoid spoofing confusion
+			//
+			// many applications handle this in different ways, but
+			// generally they'd be expected to do these two things on
+			// their own end to find the first non-spoofed IP
+			r.Header.Set("X-Forwarded-For", strings.Join(forwardedList, ","))
+		} else {
+			r.Header.Set("X-Forwarded-For", remoteIP)
+		}
+	})
+}
+
 // NoStoreCache sets the Cache-Control header to no-store for the response.
 // NoStoreCache sets the Cache-Control header to no-store for the response.
 func NoStoreCache(next http.Handler) http.Handler {
 func NoStoreCache(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {