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
+}