From 4e2c9de7085fbc8e5abe8d0659d807881d69769c Mon Sep 17 00:00:00 2001 From: Aurelia Date: Wed, 23 Apr 2025 06:06:47 +0200 Subject: [PATCH] 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 --- cmd/anubis/main.go | 1 + docs/docs/CHANGELOG.md | 1 + internal/headers.go | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index fafd1b1..b7375ea 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -280,6 +280,7 @@ func main() { h = s h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h) h = internal.XForwardedForToXRealIP(h) + h = internal.XForwardedForUpdate(h) srv := http.Server{Handler: h} listener, listenerUrl := setupListener(*bindNetwork, *bind) diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index fa538e1..71cc42a 100644 --- a/docs/docs/CHANGELOG.md +++ b/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 - Improved error handling in some edge cases - 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 diff --git a/internal/headers.go b/internal/headers.go index eb7778b..4516b40 100644 --- a/internal/headers.go +++ b/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. func NoStoreCache(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {