@marcel91200 / caddy Mirror · read-only / commits / c50094fc
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 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 ""