From 546752e2141f36ef292ef533a5e42cadaa559758 Mon Sep 17 00:00:00 2001 From: Renich Bon Ciric Date: Sun, 9 Aug 2026 23:24:13 -0600 Subject: [PATCH] feat(fastcgi): populate SERVER_ADDR by default (#7912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(fastcgi): populate SERVER_ADDR by default Populate the SERVER_ADDR FastCGI environment variable using the local socket address of the incoming connection from the request context. This improves out-of-the-box compatibility for PHP applications that rely on $_SERVER['SERVER_ADDR'] (such as legacy frameworks and custom telemetry packages) when migrating from Nginx or Apache. Co-developed-by: Gemini AI Signed-off-by: Rénich Bon Ćirić * fix(fastcgi): validate SERVER_ADDR IP format and add test coverage Validate that SERVER_ADDR is only populated when the local address parses as a valid IP address, preventing Unix socket paths from being assigned if SplitHostPort fails. Add test cases covering IPv4, IPv6, missing context, Unix sockets, and explicit overrides. Co-developed-by: Gemini AI Signed-off-by: Rénich Bon Ćirić --- .../caddyhttp/reverseproxy/fastcgi/fastcgi.go | 12 +++ .../reverseproxy/fastcgi/fastcgi_test.go | 76 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index 9b602ee5d..670259e40 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -365,6 +365,18 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) { "SCRIPT_NAME": scriptName, } + if localAddr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { + var ipStr string + if host, _, err := net.SplitHostPort(localAddr.String()); err == nil { + ipStr = host + } else { + ipStr = localAddr.String() + } + if ip := net.ParseIP(ipStr); ip != nil { + env["SERVER_ADDR"] = ipStr + } + } + // compliance with the CGI specification requires that // PATH_TRANSLATED should only exist if PATH_INFO is defined. // Info: https://www.ietf.org/rfc/rfc3875 Page 14 diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go index 2b22c813e..4173df0d9 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go @@ -1,6 +1,9 @@ package fastcgi import ( + "context" + "net" + "net/http" "strings" "testing" @@ -8,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestProvisionSplitPath(t *testing.T) { @@ -355,3 +359,75 @@ func TestSplitPosSecurityRegressionUnicodeBypass(t *testing.T) { assert.Equalf(t, -1, tr.splitPos(p), "payload %q must not be detected as .php", p) } } + +func TestBuildEnvServerAddr(t *testing.T) { + testCases := []struct { + name string + localAddr net.Addr + envVars map[string]string + expectedAddr string + expectExists bool + }{ + { + name: "IPv4 TCP listener", + localAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.12"), Port: 80}, + expectedAddr: "10.0.0.12", + expectExists: true, + }, + { + name: "IPv6 TCP listener", + localAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 443}, + expectedAddr: "2001:db8::1", + expectExists: true, + }, + { + name: "Missing local address context", + localAddr: nil, + expectExists: false, + }, + { + name: "Unix listener", + localAddr: &net.UnixAddr{Name: "/var/run/caddy.sock", Net: "unix"}, + expectExists: false, + }, + { + name: "Explicitly configured SERVER_ADDR override", + localAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.12"), Port: 80}, + envVars: map[string]string{"SERVER_ADDR": "192.168.1.100"}, + expectedAddr: "192.168.1.100", + expectExists: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tr := Transport{ + Root: "/var/www", + EnvVars: tc.envVars, + } + err := tr.Provision(caddy.Context{}) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, "http://localhost/index.php", nil) + require.NoError(t, err) + + req = req.WithContext(context.WithValue(req.Context(), caddyhttp.OriginalRequestCtxKey, *req)) + repl := caddy.NewReplacer() + req = req.WithContext(context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)) + + if tc.localAddr != nil { + req = req.WithContext(context.WithValue(req.Context(), http.LocalAddrContextKey, tc.localAddr)) + } + + env, err := tr.buildEnv(req) + require.NoError(t, err) + + val, exists := env["SERVER_ADDR"] + assert.Equal(t, tc.expectExists, exists) + if tc.expectExists { + assert.Equal(t, tc.expectedAddr, val) + } + }) + } +} +