aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAurelia <aurelia@acuteaura.net>2025-04-23 06:06:47 +0200
committerGitHub <noreply@github.com>2025-04-23 04:06:47 +0000
commit4e2c9de7085fbc8e5abe8d0659d807881d69769c (patch)
tree926011d5cbc3c124dd768b0a4a103ac0755bc8a8
parentbec7199ab6cb3f3628d6a459716d028c294a4c36 (diff)
downloadanubis-4e2c9de7085fbc8e5abe8d0659d807881d69769c.tar.xz
anubis-4e2c9de7085fbc8e5abe8d0659d807881d69769c.zip
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
-rw-r--r--cmd/anubis/main.go1
-rw-r--r--docs/docs/CHANGELOG.md1
-rw-r--r--internal/headers.go40
3 files changed, 42 insertions, 0 deletions
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) {