Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 21:45 UTC
reverseproxy: Implement trusted proxies for `X-Forwarded-*` headers (#4507)
Francis Lavoie committed
Mar 6, 2022 at 18:51 UTC
c50094fc9d34099efd705700e6d2efa2fa065412
3 files changed
+243
-23
caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies.txt
new
+56
@@ -0,0 +1,56 @@
1
+:8884
2
+
3
+reverse_proxy 127.0.0.1:65535 {
4
+ trusted_proxies 127.0.0.1
5
+}
6
+
7
+reverse_proxy 127.0.0.1:65535 {
8
+ trusted_proxies private_ranges
9
+}
10
+----------
11
+{
12
+ "apps": {
13
+ "http": {
14
+ "servers": {
15
+ "srv0": {
16
+ "listen": [
17
+ ":8884"
18
+ ],
19
+ "routes": [
20
+ {
21
+ "handle": [
22
+ {
23
+ "handler": "reverse_proxy",
24
+ "trusted_proxies": [
25
+ "127.0.0.1"
26
+ ],
27
+ "upstreams": [
28
+ {
29
+ "dial": "127.0.0.1:65535"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "handler": "reverse_proxy",
35
+ "trusted_proxies": [
36
+ "192.168.0.0/16",
37
+ "172.16.0.0/12",
38
+ "10.0.0.0/8",
39
+ "127.0.0.1/8",
40
+ "fd00::/8",
41
+ "::1"
42
+ ],
43
+ "upstreams": [
44
+ {
45
+ "dial": "127.0.0.1:65535"
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ }
53
+ }
54
+ }
55
+ }
56
+}
modules/caddyhttp/reverseproxy/caddyfile.go
+23
@@ -82,6 +82,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
82
// buffer_requests
83
//
84
// # header manipulation
85
+// trusted_proxies [private_ranges] <ranges...>
86
// header_up [+|-]<field> [<value|regexp> [<replacement>]]
87
// header_down [+|-]<field> [<value|regexp> [<replacement>]]
88
//
@@ -485,6 +486,22 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
486
}
487
h.MaxBufferSize = int64(size)
488
489
+ case "trusted_proxies":
490
+ for d.NextArg() {
491
+ if d.Val() == "private_ranges" {
492
+ h.TrustedProxies = append(h.TrustedProxies, []string{
493
+ "192.168.0.0/16",
494
+ "172.16.0.0/12",
495
+ "10.0.0.0/8",
496
+ "127.0.0.1/8",
497
+ "fd00::/8",
498
+ "::1",
499
+ }...)
500
+ continue
501
+ }
502
+ h.TrustedProxies = append(h.TrustedProxies, d.Val())
503
+ }
504
+
505
case "header_up":
506
var err error
507
@@ -504,9 +521,15 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
521
if strings.EqualFold(args[0], "host") && (args[1] == "{hostport}" || args[1] == "{http.request.hostport}") {
522
log.Printf("[WARNING] Unnecessary header_up ('Host' field): the reverse proxy's default behavior is to pass headers to the upstream")
523
}
524
+ if strings.EqualFold(args[0], "x-forwarded-for") && (args[1] == "{remote}" || args[1] == "{http.request.remote}" || args[1] == "{remote_host}" || args[1] == "{http.request.remote.host}") {
525
+ log.Printf("[WARNING] Unnecessary header_up ('X-Forwarded-For' field): the reverse proxy's default behavior is to pass headers to the upstream")
526
+ }
527
if strings.EqualFold(args[0], "x-forwarded-proto") && (args[1] == "{scheme}" || args[1] == "{http.request.scheme}") {
528
log.Printf("[WARNING] Unnecessary header_up ('X-Forwarded-Proto' field): the reverse proxy's default behavior is to pass headers to the upstream")
529
}
530
+ if strings.EqualFold(args[0], "x-forwarded-host") && (args[1] == "{host}" || args[1] == "{http.request.host}" || args[1] == "{hostport}" || args[1] == "{http.request.hostport}") {
531
+ log.Printf("[WARNING] Unnecessary header_up ('X-Forwarded-Host' field): the reverse proxy's default behavior is to pass headers to the upstream")
532
+ }
533
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], "")
534
case 3:
535
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], args[2])
modules/caddyhttp/reverseproxy/reverseproxy.go
+164
-23
@@ -90,13 +90,20 @@ type Handler struct {
90
// to the client immediately.
91
FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
92
93
+ // A list of IP ranges (supports CIDR notation) from which
94
+ // X-Forwarded-* header values should be trusted. By default,
95
+ // no proxies are trusted, so existing values will be ignored
96
+ // when setting these headers. If the proxy is trusted, then
97
+ // existing values will be used when constructing the final
98
+ // header values.
99
+ TrustedProxies []string `json:"trusted_proxies,omitempty"`
100
+
101
// Headers manipulates headers between Caddy and the backend.
102
// By default, all headers are passed-thru without changes,
103
// with the exceptions of special hop-by-hop headers.
104
//
97
- // X-Forwarded-For and X-Forwarded-Proto are also set
98
- // implicitly, but this may change in the future if the official
99
- // standardized Forwarded header field gains more adoption.
105
+ // X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host
106
+ // are also set implicitly.
107
Headers *headers.Handler `json:"headers,omitempty"`
108
109
// If true, the entire request body will be read and buffered
@@ -133,6 +140,9 @@ type Handler struct {
140
Transport http.RoundTripper `json:"-"`
141
CB CircuitBreaker `json:"-"`
142
143
+ // Holds the parsed CIDR ranges from TrustedProxies
144
+ trustedProxies []*net.IPNet
145
+
146
// Holds the named response matchers from the Caddyfile while adapting
147
responseMatchers map[string]caddyhttp.ResponseMatcher
148
@@ -192,6 +202,30 @@ func (h *Handler) Provision(ctx caddy.Context) error {
202
h.CB = mod.(CircuitBreaker)
203
}
204
205
+ // parse trusted proxy CIDRs ahead of time
206
+ for _, str := range h.TrustedProxies {
207
+ if strings.Contains(str, "/") {
208
+ _, ipNet, err := net.ParseCIDR(str)
209
+ if err != nil {
210
+ return fmt.Errorf("parsing CIDR expression: %v", err)
211
+ }
212
+ h.trustedProxies = append(h.trustedProxies, ipNet)
213
+ } else {
214
+ ip := net.ParseIP(str)
215
+ if ip == nil {
216
+ return fmt.Errorf("invalid IP address: %s", str)
217
+ }
218
+ if ipv4 := ip.To4(); ipv4 != nil {
219
+ ip = ipv4
220
+ }
221
+ mask := len(ip) * 8
222
+ h.trustedProxies = append(h.trustedProxies, &net.IPNet{
223
+ IP: ip,
224
+ Mask: net.CIDRMask(mask, mask),
225
+ })
226
+ }
227
+ }
228
+
229
// ensure any embedded headers handler module gets provisioned
230
// (see https://caddy.community/t/set-cookie-manipulation-in-reverse-proxy/7666?u=matt
231
// for what happens if we forget to provision it)
@@ -514,32 +548,103 @@ func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
548
req.Header.Set("Upgrade", reqUpType)
549
}
550
517
- if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
518
- // If we aren't the first proxy retain prior
519
- // X-Forwarded-For information as a comma+space
520
- // separated list and fold multiple headers into one.
521
- prior, ok := req.Header["X-Forwarded-For"]
522
- omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
523
- if len(prior) > 0 {
524
- clientIP = strings.Join(prior, ", ") + ", " + clientIP
525
- }
526
- if !omit {
527
- req.Header.Set("X-Forwarded-For", clientIP)
528
- }
551
+ // Add the supported X-Forwarded-* headers
552
+ err := h.addForwardedHeaders(req)
553
+ if err != nil {
554
+ return nil, err
555
+ }
556
+
557
+ return req, nil
558
+}
559
+
560
+// addForwardedHeaders adds the de-facto standard X-Forwarded-*
561
+// headers to the request before it is sent upstream.
562
+//
563
+// These headers are security sensitive, so care is taken to only
564
+// use existing values for these headers from the incoming request
565
+// if the client IP is trusted (i.e. coming from a trusted proxy
566
+// sitting in front of this server). If the request didn't have
567
+// the headers at all, then they will be added with the values
568
+// that we can glean from the request.
569
+func (h Handler) addForwardedHeaders(req *http.Request) error {
570
+ // Parse the remote IP, ignore the error as non-fatal,
571
+ // but the remote IP is required to continue, so we
572
+ // just return early. This should probably never happen
573
+ // though, unless some other module manipulated the request's
574
+ // remote address and used an invalid value.
575
+ clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
576
+ if err != nil {
577
+ // Remove the `X-Forwarded-*` headers to avoid upstreams
578
+ // potentially trusting a header that came from the client
579
+ req.Header.Del("X-Forwarded-For")
580
+ req.Header.Del("X-Forwarded-Proto")
581
+ req.Header.Del("X-Forwarded-Host")
582
+ return nil
583
+ }
584
+
585
+ // Client IP may contain a zone if IPv6, so we need
586
+ // to pull that out before parsing the IP
587
+ if idx := strings.IndexByte(clientIP, '%'); idx >= 0 {
588
+ clientIP = clientIP[:idx]
589
+ }
590
+ ip := net.ParseIP(clientIP)
591
+ if ip == nil {
592
+ return fmt.Errorf("invalid client IP address: %s", clientIP)
593
}
594
531
- prior, ok := req.Header["X-Forwarded-Proto"]
532
- omit := ok && prior == nil
533
- if len(prior) == 0 && !omit {
534
- // set X-Forwarded-Proto; many backend apps expect this too
535
- proto := "https"
536
- if req.TLS == nil {
537
- proto = "http"
595
+ // Check if the client is a trusted proxy
596
+ trusted := false
597
+ for _, ipRange := range h.trustedProxies {
598
+ if ipRange.Contains(ip) {
599
+ trusted = true
600
+ break
601
}
602
+ }
603
+
604
+ // If we aren't the first proxy, and the proxy is trusted,
605
+ // retain prior X-Forwarded-For information as a comma+space
606
+ // separated list and fold multiple headers into one.
607
+ clientXFF := clientIP
608
+ prior, ok, omit := allHeaderValues(req.Header, "X-Forwarded-For")
609
+ if trusted && ok && prior != "" {
610
+ clientXFF = prior + ", " + clientXFF
611
+ }
612
+ if !omit {
613
+ req.Header.Set("X-Forwarded-For", clientXFF)
614
+ }
615
+
616
+ // Set X-Forwarded-Proto; many backend apps expect this,
617
+ // so that they can properly craft URLs with the right
618
+ // scheme to match the original request
619
+ proto := "https"
620
+ if req.TLS == nil {
621
+ proto = "http"
622
+ }
623
+ prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Proto")
624
+ if trusted && ok && prior != "" {
625
+ proto = prior
626
+ }
627
+ if !omit {
628
req.Header.Set("X-Forwarded-Proto", proto)
629
}
630
542
- return req, nil
631
+ // Set X-Forwarded-Host; often this is redundant because
632
+ // we pass through the request Host as-is, but in situations
633
+ // where we proxy over HTTPS, the user may need to override
634
+ // Host themselves, so it's helpful to send the original too.
635
+ host, _, err := net.SplitHostPort(req.Host)
636
+ if err != nil {
637
+ host = req.Host // OK; there probably was no port
638
+ }
639
+ prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Host")
640
+ if trusted && ok && prior != "" {
641
+ host = prior
642
+ }
643
+ if !omit {
644
+ req.Header.Set("X-Forwarded-Host", host)
645
+ }
646
+
647
+ return nil
648
}
649
650
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
@@ -868,6 +973,42 @@ func copyHeader(dst, src http.Header) {
973
}
974
}
975
976
+// allHeaderValues gets all values for a given header field,
977
+// joined by a comma and space if more than one is set. If the
978
+// header field is nil, then the omit is true, meaning some
979
+// earlier logic in the server wanted to prevent this header from
980
+// getting written at all. If the header is empty, then ok is
981
+// false. Callers should still check that the value is not empty
982
+// (the header field may be set but have an empty value).
983
+func allHeaderValues(h http.Header, field string) (value string, ok bool, omit bool) {
984
+ values, ok := h[http.CanonicalHeaderKey(field)]
985
+ if ok && values == nil {
986
+ return "", true, true
987
+ }
988
+ if len(values) == 0 {
989
+ return "", false, false
990
+ }
991
+ return strings.Join(values, ", "), true, false
992
+}
993
+
994
+// lastHeaderValue gets the last value for a given header field
995
+// if more than one is set. If the header field is nil, then
996
+// the omit is true, meaning some earlier logic in the server
997
+// wanted to prevent this header from getting written at all.
998
+// If the header is empty, then ok is false. Callers should
999
+// still check that the value is not empty (the header field
1000
+// may be set but have an empty value).
1001
+func lastHeaderValue(h http.Header, field string) (value string, ok bool, omit bool) {
1002
+ values, ok := h[http.CanonicalHeaderKey(field)]
1003
+ if ok && values == nil {
1004
+ return "", true, true
1005
+ }
1006
+ if len(values) == 0 {
1007
+ return "", false, false
1008
+ }
1009
+ return values[len(values)-1], true, false
1010
+}
1011
+
1012
func upgradeType(h http.Header) string {
1013
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
1014
return ""