Skip to content

Commit 5b6e94e

Browse files
Fix trusted proxy client IP matching (#139)
1 parent 04ddeac commit 5b6e94e

2 files changed

Lines changed: 54 additions & 10 deletions

File tree

middleware.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,11 @@ func (m Defender) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
4848
if m.serveGitignore(w, r) {
4949
return nil
5050
}
51-
// Split the RemoteAddr into IP and port
52-
host, _, err := net.SplitHostPort(r.RemoteAddr)
53-
if err != nil {
54-
m.log.Error("Invalid client IP format", zap.String("ip", r.RemoteAddr))
55-
return caddyhttp.Error(http.StatusForbidden, fmt.Errorf("invalid client IP format"))
56-
}
5751

58-
clientIP := net.ParseIP(host)
59-
if clientIP == nil {
60-
m.log.Error("Invalid client IP", zap.String("ip", host))
61-
return caddyhttp.Error(http.StatusForbidden, fmt.Errorf("invalid client IP"))
52+
clientIP, err := clientIPFromRequest(r)
53+
if err != nil {
54+
m.log.Error("Invalid client IP", zap.String("remote_addr", r.RemoteAddr), zap.Error(err))
55+
return caddyhttp.Error(http.StatusForbidden, err)
6256
}
6357
m.log.Debug("Ranges", zap.Strings("ranges", m.Ranges))
6458

@@ -72,3 +66,23 @@ func (m Defender) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
7266
// Request should be blocked
7367
return m.responder.ServeHTTP(w, r, next)
7468
}
69+
70+
func clientIPFromRequest(r *http.Request) (net.IP, error) {
71+
if clientIP, ok := caddyhttp.GetVar(r.Context(), caddyhttp.ClientIPVarKey).(string); ok && clientIP != "" {
72+
return parseClientIP(clientIP)
73+
}
74+
75+
host, _, err := net.SplitHostPort(r.RemoteAddr)
76+
if err != nil {
77+
return nil, fmt.Errorf("invalid client IP format")
78+
}
79+
return parseClientIP(host)
80+
}
81+
82+
func parseClientIP(rawIP string) (net.IP, error) {
83+
clientIP := net.ParseIP(rawIP)
84+
if clientIP == nil {
85+
return nil, fmt.Errorf("invalid client IP")
86+
}
87+
return clientIP, nil
88+
}

middleware_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"github.com/caddyserver/caddy/v2"
11+
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
1112
"github.com/stretchr/testify/require"
1213
"go.uber.org/zap"
1314
"pkg.jsn.cam/caddy-defender/responders"
@@ -226,3 +227,32 @@ func TestDefenderServeHTTP_RobotsFile(t *testing.T) {
226227
require.Contains(t, recorder.Body.String(), "User-agent: *")
227228
require.Contains(t, recorder.Body.String(), "Disallow: /")
228229
}
230+
231+
// Regression test for GHSA-3h23-rrpc-3p87: Caddy resolves the real client IP
232+
// into ClientIPVarKey, and Defender must not fall back to the proxy RemoteAddr.
233+
func TestDefenderServeHTTP_UsesCaddyClientIP(t *testing.T) {
234+
defender := &Defender{
235+
RawResponder: "block",
236+
Ranges: []string{"203.0.113.0/24"},
237+
responder: &responders.BlockResponder{},
238+
}
239+
240+
ctx := caddy.Context{Context: context.Background()}
241+
defender.log = zap.NewNop()
242+
err := defender.Provision(ctx)
243+
require.NoError(t, err)
244+
245+
req := httptest.NewRequest(http.MethodGet, "/", nil)
246+
req.RemoteAddr = "198.51.100.1:12345"
247+
req = req.WithContext(context.WithValue(req.Context(), caddyhttp.VarsCtxKey, map[string]any{
248+
caddyhttp.ClientIPVarKey: "203.0.113.10",
249+
}))
250+
251+
recorder := httptest.NewRecorder()
252+
253+
err = defender.ServeHTTP(recorder, req, &mockHandler{})
254+
255+
require.NoError(t, err)
256+
require.Equal(t, http.StatusForbidden, recorder.Code)
257+
require.Equal(t, "Access denied", recorder.Body.String())
258+
}

0 commit comments

Comments
 (0)