@marcel91200 / caddy Mirror · read-only / commits / cc0c0cf0
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 18:15 UTC

caddyhttp: Security enhancements for client IP parsing (#5805)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

Nebez Briefkani committed Jan 13, 2024 at 12:46 UTC cc0c0cf03e3ebdd1377aaa0b8ad6c0b39e880955
3 files changed +352 -5
caddyconfig/httpcaddyfile/serveroptions.go
+8
@@ -46,6 +46,7 @@ type serverOptions struct {
46 Protocols []string
47 StrictSNIHost *bool
48 TrustedProxiesRaw json.RawMessage
49 + TrustedProxiesStrict int
50 ClientIPHeaders []string
51 ShouldLogCredentials bool
52 Metrics *caddyhttp.Metrics
@@ -217,6 +218,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
218 )
219 serverOpts.TrustedProxiesRaw = jsonSource
220
221 + case "trusted_proxies_strict":
222 + if d.NextArg() {
223 + return nil, d.ArgErr()
224 + }
225 + serverOpts.TrustedProxiesStrict = 1
226 +
227 case "client_ip_headers":
228 headers := d.RemainingArgs()
229 for _, header := range headers {
@@ -340,6 +347,7 @@ func applyServerOptions(
347 server.StrictSNIHost = opts.StrictSNIHost
348 server.TrustedProxiesRaw = opts.TrustedProxiesRaw
349 server.ClientIPHeaders = opts.ClientIPHeaders
350 + server.TrustedProxiesStrict = opts.TrustedProxiesStrict
351 server.Metrics = opts.Metrics
352 if opts.ShouldLogCredentials {
353 if server.Logs == nil {
modules/caddyhttp/server.go
+52 -5
@@ -173,6 +173,19 @@ type Server struct {
173 // remote IP address.
174 ClientIPHeaders []string `json:"client_ip_headers,omitempty"`
175
176 + // If greater than zero, enables strict ClientIPHeaders
177 + // (default X-Forwarded-For) parsing. If enabled, the
178 + // ClientIPHeaders will be parsed from right to left, and
179 + // the first value that is both valid and doesn't match the
180 + // trusted proxy list will be used as client IP. If zero,
181 + // the ClientIPHeaders will be parsed from left to right,
182 + // and the first value that is a valid IP address will be
183 + // used as client IP.
184 + //
185 + // This depends on `trusted_proxies` being configured.
186 + // This option is disabled by default.
187 + TrustedProxiesStrict int `json:"trusted_proxies_strict,omitempty"`
188 +
189 // Enables access logging and configures how access logs are handled
190 // in this server. To minimally enable access logs, simply set this
191 // to a non-null, empty struct.
@@ -839,17 +852,28 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) {
852 if s.trustedProxies == nil {
853 return false, ipAddr.String()
854 }
842 - for _, ipRange := range s.trustedProxies.GetIPRanges(r) {
843 - if ipRange.Contains(ipAddr) {
844 - // We trust the proxy, so let's try to
845 - // determine the real client IP
846 - return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String())
855 +
856 + if isTrustedClientIP(ipAddr, s.trustedProxies.GetIPRanges(r)) {
857 + if s.TrustedProxiesStrict > 0 {
858 + return true, strictUntrustedClientIp(r, s.ClientIPHeaders, s.trustedProxies.GetIPRanges(r), ipAddr.String())
859 }
860 + return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String())
861 }
862
863 return false, ipAddr.String()
864 }
865
866 +// isTrustedClientIP returns true if the given IP address is
867 +// in the list of trusted IP ranges.
868 +func isTrustedClientIP(ipAddr netip.Addr, trusted []netip.Prefix) bool {
869 + for _, ipRange := range trusted {
870 + if ipRange.Contains(ipAddr) {
871 + return true
872 + }
873 + }
874 + return false
875 +}
876 +
877 // trustedRealClientIP finds the client IP from the request assuming it is
878 // from a trusted client. If there is no client IP headers, then the
879 // direct remote address is returned. If there are client IP headers,
@@ -884,6 +908,29 @@ func trustedRealClientIP(r *http.Request, headers []string, clientIP string) str
908 return clientIP
909 }
910
911 +// strictUntrustedClientIp iterates through the list of client IP headers,
912 +// parses them from right-to-left, and returns the first valid IP address
913 +// that is untrusted. If no valid IP address is found, then the direct
914 +// remote address is returned.
915 +func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.Prefix, clientIP string) string {
916 + for _, headerName := range headers {
917 + ips := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
918 +
919 + for i := len(ips) - 1; i >= 0; i-- {
920 + ip, _, _ := strings.Cut(strings.TrimSpace(ips[i]), "%")
921 + ipAddr, err := netip.ParseAddr(ip)
922 + if err != nil {
923 + continue
924 + }
925 + if !isTrustedClientIP(ipAddr, trusted) {
926 + return ipAddr.String()
927 + }
928 + }
929 + }
930 +
931 + return clientIP
932 +}
933 +
934 // cloneURL makes a copy of r.URL and returns a
935 // new value that doesn't reference the original.
936 func cloneURL(from, to *url.URL) {
modules/caddyhttp/server_test.go
+292
@@ -6,6 +6,7 @@ import (
6 "io"
7 "net/http"
8 "net/http/httptest"
9 + "net/netip"
10 "testing"
11 "time"
12
@@ -144,3 +145,294 @@ func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) {
145 s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false)
146 }
147 }
148 +func TestServer_TrustedRealClientIP_NoTrustedHeaders(t *testing.T) {
149 + req := httptest.NewRequest("GET", "/", nil)
150 + req.RemoteAddr = "192.0.2.1:12345"
151 + ip := trustedRealClientIP(req, []string{}, "192.0.2.1")
152 +
153 + assert.Equal(t, ip, "192.0.2.1")
154 +}
155 +
156 +func TestServer_TrustedRealClientIP_OneTrustedHeaderEmpty(t *testing.T) {
157 + req := httptest.NewRequest("GET", "/", nil)
158 + req.RemoteAddr = "192.0.2.1:12345"
159 + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1")
160 +
161 + assert.Equal(t, ip, "192.0.2.1")
162 +}
163 +
164 +func TestServer_TrustedRealClientIP_OneTrustedHeaderInvalid(t *testing.T) {
165 + req := httptest.NewRequest("GET", "/", nil)
166 + req.RemoteAddr = "192.0.2.1:12345"
167 + req.Header.Set("X-Forwarded-For", "not, an, ip")
168 + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1")
169 +
170 + assert.Equal(t, ip, "192.0.2.1")
171 +}
172 +
173 +func TestServer_TrustedRealClientIP_OneTrustedHeaderValid(t *testing.T) {
174 + req := httptest.NewRequest("GET", "/", nil)
175 + req.RemoteAddr = "192.0.2.1:12345"
176 + req.Header.Set("X-Forwarded-For", "10.0.0.1")
177 + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1")
178 +
179 + assert.Equal(t, ip, "10.0.0.1")
180 +}
181 +
182 +func TestServer_TrustedRealClientIP_OneTrustedHeaderValidArray(t *testing.T) {
183 + req := httptest.NewRequest("GET", "/", nil)
184 + req.RemoteAddr = "192.0.2.1:12345"
185 + req.Header.Set("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3")
186 + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1")
187 +
188 + assert.Equal(t, ip, "1.1.1.1")
189 +}
190 +
191 +func TestServer_TrustedRealClientIP_SkipsInvalidIps(t *testing.T) {
192 + req := httptest.NewRequest("GET", "/", nil)
193 + req.RemoteAddr = "192.0.2.1:12345"
194 + req.Header.Set("X-Forwarded-For", "not an ip, bad bad, 10.0.0.1")
195 + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1")
196 +
197 + assert.Equal(t, ip, "10.0.0.1")
198 +}
199 +
200 +func TestServer_TrustedRealClientIP_MultipleTrustedHeaderValidArray(t *testing.T) {
201 + req := httptest.NewRequest("GET", "/", nil)
202 + req.RemoteAddr = "192.0.2.1:12345"
203 + req.Header.Set("Real-Client-IP", "1.1.1.1, 2.2.2.2, 3.3.3.3")
204 + req.Header.Set("X-Forwarded-For", "3.3.3.3, 4.4.4.4")
205 + ip1 := trustedRealClientIP(req, []string{"X-Forwarded-For", "Real-Client-IP"}, "192.0.2.1")
206 + ip2 := trustedRealClientIP(req, []string{"Real-Client-IP", "X-Forwarded-For"}, "192.0.2.1")
207 + ip3 := trustedRealClientIP(req, []string{"Missing-Header-IP", "Real-Client-IP", "X-Forwarded-For"}, "192.0.2.1")
208 +
209 + assert.Equal(t, ip1, "3.3.3.3")
210 + assert.Equal(t, ip2, "1.1.1.1")
211 + assert.Equal(t, ip3, "1.1.1.1")
212 +}
213 +
214 +func TestServer_DetermineTrustedProxy_NoConfig(t *testing.T) {
215 + server := &Server{}
216 +
217 + req := httptest.NewRequest("GET", "/", nil)
218 + req.RemoteAddr = "192.0.2.1:12345"
219 +
220 + trusted, clientIP := determineTrustedProxy(req, server)
221 +
222 + assert.False(t, trusted)
223 + assert.Equal(t, clientIP, "192.0.2.1")
224 +}
225 +
226 +func TestServer_DetermineTrustedProxy_NoConfigIpv6(t *testing.T) {
227 + server := &Server{}
228 +
229 + req := httptest.NewRequest("GET", "/", nil)
230 + req.RemoteAddr = "[::1]:12345"
231 +
232 + trusted, clientIP := determineTrustedProxy(req, server)
233 +
234 + assert.False(t, trusted)
235 + assert.Equal(t, clientIP, "::1")
236 +}
237 +
238 +func TestServer_DetermineTrustedProxy_NoConfigIpv6Zones(t *testing.T) {
239 + server := &Server{}
240 +
241 + req := httptest.NewRequest("GET", "/", nil)
242 + req.RemoteAddr = "[::1%eth2]:12345"
243 +
244 + trusted, clientIP := determineTrustedProxy(req, server)
245 +
246 + assert.False(t, trusted)
247 + assert.Equal(t, clientIP, "::1")
248 +}
249 +
250 +func TestServer_DetermineTrustedProxy_TrustedLoopback(t *testing.T) {
251 + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8")
252 +
253 + server := &Server{
254 + trustedProxies: &StaticIPRange{
255 + ranges: []netip.Prefix{loopbackPrefix},
256 + },
257 + ClientIPHeaders: []string{"X-Forwarded-For"},
258 + }
259 +
260 + req := httptest.NewRequest("GET", "/", nil)
261 + req.RemoteAddr = "127.0.0.1:12345"
262 + req.Header.Set("X-Forwarded-For", "31.40.0.10")
263 +
264 + trusted, clientIP := determineTrustedProxy(req, server)
265 +
266 + assert.True(t, trusted)
267 + assert.Equal(t, clientIP, "31.40.0.10")
268 +}
269 +
270 +func TestServer_DetermineTrustedProxy_UntrustedPrefix(t *testing.T) {
271 + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8")
272 +
273 + server := &Server{
274 + trustedProxies: &StaticIPRange{
275 + ranges: []netip.Prefix{loopbackPrefix},
276 + },
277 + ClientIPHeaders: []string{"X-Forwarded-For"},
278 + }
279 +
280 + req := httptest.NewRequest("GET", "/", nil)
281 + req.RemoteAddr = "10.0.0.1:12345"
282 + req.Header.Set("X-Forwarded-For", "31.40.0.10")
283 +
284 + trusted, clientIP := determineTrustedProxy(req, server)
285 +
286 + assert.False(t, trusted)
287 + assert.Equal(t, clientIP, "10.0.0.1")
288 +}
289 +
290 +func TestServer_DetermineTrustedProxy_MultipleTrustedPrefixes(t *testing.T) {
291 + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8")
292 + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
293 +
294 + server := &Server{
295 + trustedProxies: &StaticIPRange{
296 + ranges: []netip.Prefix{loopbackPrefix, localPrivatePrefix},
297 + },
298 + ClientIPHeaders: []string{"X-Forwarded-For"},
299 + }
300 +
301 + req := httptest.NewRequest("GET", "/", nil)
302 + req.RemoteAddr = "10.0.0.1:12345"
303 + req.Header.Set("X-Forwarded-For", "31.40.0.10")
304 +
305 + trusted, clientIP := determineTrustedProxy(req, server)
306 +
307 + assert.True(t, trusted)
308 + assert.Equal(t, clientIP, "31.40.0.10")
309 +}
310 +
311 +func TestServer_DetermineTrustedProxy_MultipleTrustedClientHeaders(t *testing.T) {
312 + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8")
313 + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
314 +
315 + server := &Server{
316 + trustedProxies: &StaticIPRange{
317 + ranges: []netip.Prefix{loopbackPrefix, localPrivatePrefix},
318 + },
319 + ClientIPHeaders: []string{"CF-Connecting-IP", "X-Forwarded-For"},
320 + }
321 +
322 + req := httptest.NewRequest("GET", "/", nil)
323 + req.RemoteAddr = "10.0.0.1:12345"
324 + req.Header.Set("CF-Connecting-IP", "1.1.1.1, 2.2.2.2")
325 + req.Header.Set("X-Forwarded-For", "3.3.3.3, 4.4.4.4")
326 +
327 + trusted, clientIP := determineTrustedProxy(req, server)
328 +
329 + assert.True(t, trusted)
330 + assert.Equal(t, clientIP, "1.1.1.1")
331 +}
332 +
333 +func TestServer_DetermineTrustedProxy_MatchLeftMostValidIp(t *testing.T) {
334 + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
335 +
336 + server := &Server{
337 + trustedProxies: &StaticIPRange{
338 + ranges: []netip.Prefix{localPrivatePrefix},
339 + },
340 + ClientIPHeaders: []string{"X-Forwarded-For"},
341 + }
342 +
343 + req := httptest.NewRequest("GET", "/", nil)
344 + req.RemoteAddr = "10.0.0.1:12345"
345 + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.1")
346 +
347 + trusted, clientIP := determineTrustedProxy(req, server)
348 +
349 + assert.True(t, trusted)
350 + assert.Equal(t, clientIP, "30.30.30.30")
351 +}
352 +
353 +func TestServer_DetermineTrustedProxy_MatchRightMostUntrusted(t *testing.T) {
354 + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
355 +
356 + server := &Server{
357 + trustedProxies: &StaticIPRange{
358 + ranges: []netip.Prefix{localPrivatePrefix},
359 + },
360 + ClientIPHeaders: []string{"X-Forwarded-For"},
361 + TrustedProxiesStrict: 1,
362 + }
363 +
364 + req := httptest.NewRequest("GET", "/", nil)
365 + req.RemoteAddr = "10.0.0.1:12345"
366 + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.1")
367 +
368 + trusted, clientIP := determineTrustedProxy(req, server)
369 +
370 + assert.True(t, trusted)
371 + assert.Equal(t, clientIP, "45.54.45.54")
372 +}
373 +
374 +func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedSkippingEmpty(t *testing.T) {
375 + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
376 +
377 + server := &Server{
378 + trustedProxies: &StaticIPRange{
379 + ranges: []netip.Prefix{localPrivatePrefix},
380 + },
381 + ClientIPHeaders: []string{"Missing-Header", "CF-Connecting-IP", "X-Forwarded-For"},
382 + TrustedProxiesStrict: 1,
383 + }
384 +
385 + req := httptest.NewRequest("GET", "/", nil)
386 + req.RemoteAddr = "10.0.0.1:12345"
387 + req.Header.Set("CF-Connecting-IP", "not a real IP")
388 + req.Header.Set("X-Forwarded-For", "30.30.30.30, bad, 45.54.45.54, not real")
389 +
390 + trusted, clientIP := determineTrustedProxy(req, server)
391 +
392 + assert.True(t, trusted)
393 + assert.Equal(t, clientIP, "45.54.45.54")
394 +}
395 +
396 +func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedSkippingTrusted(t *testing.T) {
397 + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
398 +
399 + server := &Server{
400 + trustedProxies: &StaticIPRange{
401 + ranges: []netip.Prefix{localPrivatePrefix},
402 + },
403 + ClientIPHeaders: []string{"CF-Connecting-IP", "X-Forwarded-For"},
404 + TrustedProxiesStrict: 1,
405 + }
406 +
407 + req := httptest.NewRequest("GET", "/", nil)
408 + req.RemoteAddr = "10.0.0.1:12345"
409 + req.Header.Set("CF-Connecting-IP", "10.0.0.1, 10.0.0.2, 10.0.0.3")
410 + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.4")
411 +
412 + trusted, clientIP := determineTrustedProxy(req, server)
413 +
414 + assert.True(t, trusted)
415 + assert.Equal(t, clientIP, "45.54.45.54")
416 +}
417 +
418 +func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedFirst(t *testing.T) {
419 + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
420 +
421 + server := &Server{
422 + trustedProxies: &StaticIPRange{
423 + ranges: []netip.Prefix{localPrivatePrefix},
424 + },
425 + ClientIPHeaders: []string{"CF-Connecting-IP", "X-Forwarded-For"},
426 + TrustedProxiesStrict: 1,
427 + }
428 +
429 + req := httptest.NewRequest("GET", "/", nil)
430 + req.RemoteAddr = "10.0.0.1:12345"
431 + req.Header.Set("CF-Connecting-IP", "10.0.0.1, 90.100.110.120, 10.0.0.2, 10.0.0.3")
432 + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.4")
433 +
434 + trusted, clientIP := determineTrustedProxy(req, server)
435 +
436 + assert.True(t, trusted)
437 + assert.Equal(t, clientIP, "90.100.110.120")
438 +}