Files
caddy/modules/caddyhttp/reverseproxy/headers_test.go
XYenon 03e6e439dd reverseproxy: fix X-Forwarded-* headers for Unix socket requests (#7463)
When a request arrives via a Unix domain socket (RemoteAddr == "@"),
net.SplitHostPort fails, causing addForwardedHeaders to strip all
X-Forwarded-* headers even when the connection is trusted via
trusted_proxies_unix.

Handle Unix socket connections before parsing RemoteAddr: if untrusted,
strip headers for security; if trusted, let clientIP remain empty (no
peer IP for a Unix socket hop) and fall through to the shared header
logic, preserving the existing XFF chain without appending a spurious
entry.

Amp-Thread-ID: https://ampcode.com/threads/T-019c4225-a0ad-7283-ac56-e2c01eae1103

Co-authored-by: Amp <amp@ampcode.com>
2026-02-10 13:00:20 -07:00

128 lines
3.9 KiB
Go

package reverseproxy
import (
"context"
"net/http/httptest"
"testing"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestAddForwardedHeadersNonIP(t *testing.T) {
h := Handler{}
// Simulate a request with a non-IP remote address (e.g. SCION, abstract socket, or hostname)
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = "my-weird-network:12345"
// Mock the context variables required by Caddy.
// We need to inject the variable map manually since we aren't running the full server.
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: false,
}
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
req = req.WithContext(ctx)
// Execute the unexported function
err := h.addForwardedHeaders(req)
// Expectation: No error should be returned for non-IP addresses.
// The function should simply skip the trusted proxy check.
if err != nil {
t.Errorf("expected no error for non-IP address, got: %v", err)
}
}
func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) {
h := Handler{}
req := httptest.NewRequest("GET", "http://example.com/", nil)
req.RemoteAddr = "@"
req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.0.0.1")
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "original.example.com")
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: true,
caddyhttp.ClientIPVarKey: "1.2.3.4",
}
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
req = req.WithContext(ctx)
err := h.addForwardedHeaders(req)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if got := req.Header.Get("X-Forwarded-For"); got != "1.2.3.4, 10.0.0.1" {
t.Errorf("X-Forwarded-For = %q, want %q", got, "1.2.3.4, 10.0.0.1")
}
if got := req.Header.Get("X-Forwarded-Proto"); got != "https" {
t.Errorf("X-Forwarded-Proto = %q, want %q", got, "https")
}
if got := req.Header.Get("X-Forwarded-Host"); got != "original.example.com" {
t.Errorf("X-Forwarded-Host = %q, want %q", got, "original.example.com")
}
}
func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) {
h := Handler{}
req := httptest.NewRequest("GET", "http://example.com/", nil)
req.RemoteAddr = "@"
req.Header.Set("X-Forwarded-For", "1.2.3.4")
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "spoofed.example.com")
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: false,
caddyhttp.ClientIPVarKey: "",
}
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
req = req.WithContext(ctx)
err := h.addForwardedHeaders(req)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if got := req.Header.Get("X-Forwarded-For"); got != "" {
t.Errorf("X-Forwarded-For should be deleted, got %q", got)
}
if got := req.Header.Get("X-Forwarded-Proto"); got != "" {
t.Errorf("X-Forwarded-Proto should be deleted, got %q", got)
}
if got := req.Header.Get("X-Forwarded-Host"); got != "" {
t.Errorf("X-Forwarded-Host should be deleted, got %q", got)
}
}
func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
h := Handler{}
req := httptest.NewRequest("GET", "http://example.com/", nil)
req.RemoteAddr = "@"
vars := map[string]interface{}{
caddyhttp.TrustedProxyVarKey: true,
caddyhttp.ClientIPVarKey: "5.6.7.8",
}
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
req = req.WithContext(ctx)
err := h.addForwardedHeaders(req)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if got := req.Header.Get("X-Forwarded-For"); got != "" {
t.Errorf("X-Forwarded-For should be empty when no prior XFF exists, got %q", got)
}
if got := req.Header.Get("X-Forwarded-Proto"); got != "http" {
t.Errorf("X-Forwarded-Proto = %q, want %q", got, "http")
}
if got := req.Header.Get("X-Forwarded-Host"); got != "example.com" {
t.Errorf("X-Forwarded-Host = %q, want %q", got, "example.com")
}
}