CVE-2021-32783 (Go, published 2021-07-23T22:15Z)
Description: value: Contour is a Kubernetes ingress controller using Envoy proxy. In Contour before version 1.17.1 a specially crafted ExternalName type Service may be used to access Envoy's admin interface, which Contour normally prevents from access outside the Envoy container. This can be used to shut down Envoy remotely (a denial of service), or to expose the existence of any Secret that Envoy is using for its configuration, including most notably TLS Keypairs. However, it *cannot* be used to get the *content* of those secrets. Since this attack allows access to the administration interface, a variety of administration options are available, such as shutting down the Envoy or draining traffic. In general, the Envoy admin interface cannot easily be used for making changes to the cluster, in-flight requests, or backend services, but it could be used to shut down or drain Envoy, change traffic routing, or to retrieve secret metadata, as mentioned above. The issue will be addressed in Contour v1.18.0 and a cherry-picked patch release, v1.17.1, has been released to cover users who cannot upgrade at this time. For more details refer to the linked GitHub Security Advisory.
Severity: MEDIUM (CVSS v3 base score: 8.5)
Weakness: CWE-610 \u2014 Externally Controlled Reference to a Resource in Another Sphere
The product uses an externally controlled name or reference that resolves to a resource that is outside of the intended control sphere.
Repository: https://github.com/projectcontour/contour
Files changed:
cmd/contour/serve.go
examples/contour/01-contour-config.yaml
examples/render/contour-gateway.yaml
examples/render/contour.yaml
go.mod
internal/dag/accessors.go
internal/dag/accessors_test.go
internal/dag/builder_test.go
internal/dag/extension_processor.go
internal/dag/gatewayapi_processor.go
internal/dag/httpproxy_processor.go
internal/dag/ingress_processor.go
internal/dag/status_test.go
internal/featuretests/v3/externalname_test.go
internal/featuretests/v3/headerpolicy_test.go
pkg/config/parameters.go
site/content/docs/main/configuration.md
test/e2e/httpproxy/018_external_name_test.go
test/e2e/httpproxy/httpproxy_test.go
Commit message: cherrypicks for v1.17.1 (#3909)

* Merge pull request from GHSA-5ph6-qq5x-7jwc

Signed-off-by: Nick Young <ynick@vmware.com>

* Fix spelling and lint errors that slipped into the ExternalName PR (#3908)

Signed-off-by: Nick Young <ynick@vmware.com>

Co-authored-by: Nick Young <ynick@vmware.com>

Vulnerable code:
			FieldLogger:       log.WithField("context", "IngressProcessor"),
			ClientCertificate: clientCert,
			DisablePermitInsecure: ctx.Config.DisablePermitInsecure,
			FallbackCertificate:   fallbackCert,
			DNSLookupFamily:       ctx.Config.Cluster.DNSLookupFamily,
			ClientCertificate:     clientCert,
			RequestHeadersPolicy:  &requestHeadersPolicy,
			ResponseHeadersPolicy: &responseHeadersPolicy,
			FieldLogger: log.WithField("context", "GatewayAPIProcessor"),
func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache) (*Service, error) {
		{Name: "service1", Namespace: "default"}: s1,
		port    intstr.IntOrString
		want    *Service
		wantErr error
			got, gotErr := dag.EnsureService(tc.NamespacedName, tc.port, &b.Source)
			want: listeners(),
						FieldLogger: fixture.NewTestLogger(t),
						DisablePermitInsecure: tc.disablePermitInsecure,
	service, err := p.dag.EnsureService(meta, intstr.FromInt(int(*port)), p.source)
		return nil, fmt.Errorf("service %q does not exist", meta.Name)
			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source)
			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source)
				validCond.AddErrorf(contour_api_v1.ConditionTypeTCPProxyError, "UnresolvedServiceRef",
		s, err := p.dag.EnsureService(m, port, p.source)
				WithError(contour_api_v1.ConditionTypeTCPProxyError, "UnresolvedServiceRef", `Spec.TCPProxy unresolved service reference: service "roots/not-found" not found`),
				WithError(contour_api_v1.ConditionTypeTCPProxyError, "UnresolvedServiceRef", `Spec.TCPProxy unresolved service reference: port "9999" on service "roots/kuard" not matched`),
					Message: "service \"invalid-one\" does not exist, service \"invalid-two\" does not exist",
						Message: "service \"invalid-one\" does not exist, service \"invalid-two\" does not exist",
	rh, c, done := setup(t)
	rh, c, done := setup(t)
		f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
		f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
	f.NamespacedTest("018-external-name-service-insecure", testExternalNameServiceInsecure)
	f.NamespacedTest("018-external-name-service-tls", testExternalNameServiceTLS)
	return proxy != nil && proxy.Status.CurrentStatus == "valid"

Fixed code:
	log.Debugf("EnableExternalNameService is set to %t", ctx.Config.EnableExternalNameService)
			EnableExternalNameService: ctx.Config.EnableExternalNameService,
			FieldLogger:               log.WithField("context", "IngressProcessor"),
			ClientCertificate:         clientCert,
			// Note that ExtensionService does not support ExternalName, if it does get added,
			// need to bring EnableExternalNameService in here too.
			EnableExternalNameService: ctx.Config.EnableExternalNameService,
			DisablePermitInsecure:     ctx.Config.DisablePermitInsecure,
			FallbackCertificate:       fallbackCert,
			DNSLookupFamily:           ctx.Config.Cluster.DNSLookupFamily,
			ClientCertificate:         clientCert,
			RequestHeadersPolicy:      &requestHeadersPolicy,
			ResponseHeadersPolicy:     &responseHeadersPolicy,
			EnableExternalNameService: ctx.Config.EnableExternalNameService,
			FieldLogger:               log.WithField("context", "GatewayAPIProcessor"),
    ####
    # ExternalName Services are disabled by default due to CVE-2021-XXXXX
    # You can re-enable them by setting this setting to `true`.
    # This is not recommended without understanding the security implications.
    # Please see the advisory at https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for the details.
    # enableExternalNameService: false
    ## 
    ####
    # ExternalName Services are disabled by default due to CVE-2021-XXXXX
    # You can re-enable them by setting this setting to `true`.
    # This is not recommended without understanding the security implications.
    # Please see the advisory at https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for the details.
    # enableExternalNameService: false
    ##
    ####
    # ExternalName Services are disabled by default due to CVE-2021-XXXXX
    # You can re-enable them by setting this setting to `true`.
    # This is not recommended without understanding the security implications.
    # Please see the advisory at https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for the details.
    # enableExternalNameService: false
    ##
	github.com/davecgh/go-spew v1.1.1 // indirect
func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache, enableExternalNameSvc bool) (*Service, error) {
	err = validateExternalName(svc, enableExternalNameSvc)
	if err != nil {
		return nil, err
	}

func validateExternalName(svc *v1.Service, enableExternalNameSvc bool) error {

	// If this isn't an ExternalName Service, we're all good here.
	en := externalName(svc)
	if en == "" {
		return nil
	}

	// If ExternalNames are disabled, then we don't want to add this to the DAG.
	if !enableExternalNameSvc {
		return fmt.Errorf("%s/%s is an ExternalName service, these are not currently enabled. See the config.enableExternalNameService config file setting", svc.Namespace, svc.Name)
	}

	// Check against a list of known localhost names, using a map to approximate a set.
	// TODO(youngnick) This is a very porous hack, and we should probably look into doing a DNS
	// lookup to check what the externalName resolves to, but I'm worried about the
	// performance impact of doing one or more DNS lookups per DAG run, so we're
	// going to go with a specific blocklist for now.
	localhostNames := map[string]struct{}{
		"localhost":               {},
		"localhost.localdomain":   {},
		"local.projectcontour.io": {},
	}

	_, localhost := localhostNames[en]
	if localhost {
		return fmt.Errorf("%s/%s is an ExternalName service that points to localhost, this is not allowed", svc.Namespace, svc.Name)
	}

	return nil
}


	externalNameValid := &v1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "externalnamevalid",
			Namespace: "default",
		},
		Spec: v1.ServiceSpec{
			Type:         v1.ServiceTypeExternalName,
			ExternalName: "external.projectcontour.io",
			Ports: []v1.ServicePort{{
				Name:       "http",
				Protocol:   "TCP",
				Port:       80,
				TargetPort: intstr.FromInt(80),
			}},
		},
	}

	externalNameLocalhost := &v1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "externalnamelocalhost",
			Namespace: "default",
		},
		Spec: v1.ServiceSpec{
			Type:         v1.ServiceTypeExternalName,
			ExternalName: "localhost",
			Ports: []v1.ServicePort{{
				Name:       "http",
				Protocol:   "TCP",
				Port:       80,
				TargetPort: intstr.FromInt(80),
			}},
		},
	}

		{Name: "service1", Namespace: "default"}:              s1,
		{Name: "externalnamevalid", Namespace: "default"}:     externalNameValid,
		{Name: "externalnamelocalhost", Namespace: "default"}: externalNameLocalhost,
		port                  intstr.IntOrString
		enableExternalNameSvc bool
		want                  *Service
		wantErr               error
		"When ExternalName Services are not disabled no error is returned": {
			NamespacedName: types.NamespacedName{Name: "externalnamevalid", Namespace: "default"},
			port:           intstr.FromString("80"),
			want: &Service{
				Weighted: WeightedService{
					Weight:           1,
					ServiceName:      "externalnamevalid",
					ServiceNamespace: "default",
					ServicePort: v1.ServicePort{
						Name:       "http",
						Protocol:   "TCP",
						Port:       80,
						TargetPort: intstr.FromInt(80),
					},
				},
				ExternalName: "external.projectcontour.io",
			},
			enableExternalNameSvc: true,
		},
		"When ExternalName Services are disabled an error is returned": {
			NamespacedName: types.NamespacedName{Name: "externalnamevalid", Namespace: "default"},
			port:           intstr.FromString("80"),
			wantErr:        errors.New(`default/externalnamevalid is an ExternalName service, these are not currently enabled. See the config.enableExternalNameService config file setting`),
		},
		"When ExternalName Services are enabled but a localhost ExternalName is used an error is returned": {
			NamespacedName:        types.NamespacedName{Name: "externalnamelocalhost", Namespace: "default"},
			port:                  intstr.FromString("80"),
			wantErr:               errors.New(`default/externalnamelocalhost is an ExternalName service that points to localhost, this is not allowed`),
			enableExternalNameSvc: true,
		},
			got, gotErr := dag.EnsureService(tc.NamespacedName, tc.port, &b.Source, tc.enableExternalNameSvc)
	ingressExternalNameService := &networking_v1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "externalname",
			Namespace: "default",
		},
		Spec: networking_v1.IngressSpec{
			Rules: []networking_v1.IngressRule{{
				Host: "example.com",
				IngressRuleValue: networking_v1.IngressRuleValue{
					HTTP: &networking_v1.HTTPIngressRuleValue{
						Paths: []networking_v1.HTTPIngressPath{{
							Backend: *backendv1(s14.GetName(), intstr.FromInt(80)),
						}},
					},
				},
			}},
		},
	}

		enableExternalNameSvc        bool
		"insert ingress with externalName service": {
			objs: []interface{}{
				ingressExternalNameService,
				s14,
			},
			enableExternalNameSvc: true,
			want: listeners(
				&Listener{
					Port: 80,
					VirtualHosts: virtualhosts(
						virtualhost("example.com", &Route{
							PathMatchCondition: prefixString("/"),
							Clusters: []*Cluster{{
								Upstream: &Service{
									ExternalName: "externalservice.io",
									Weighted: WeightedService{
										Weight:           1,
										ServiceName:      s14.Name,
										ServiceNamespace: s14.Namespace,
										ServicePort:      s14.Spec.Ports[0],
									},
								},
							}},
						}),
					),
				},
			),
		},
		"insert ingress with externalName service, but externalName services disabled": {
			objs: []interface{}{
				ingressExternalNameService,
				s14,
			},
			enableExternalNameSvc: false,
			want:                  listeners(),
		},
			enableExternalNameSvc: true,
			enableExternalNameSvc: true,
			enableExternalNameSvc: true,
			enableExternalNameSvc: true,
			want:                  listeners(),
						FieldLogger:               fixture.NewTestLogger(t),
						EnableExternalNameService: tc.enableExternalNameSvc,
						EnableExternalNameService: tc.enableExternalNameSvc,
						DisablePermitInsecure:     tc.disablePermitInsecure,
		// TODO(youngnick): If ExternalName support is added, we must pass down the EnableExternalNameService bool
		// and check it first.

	// EnableExternalNameService allows processing of ExternalNameServices
	// This is normally disabled for security reasons.
	// See https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for details.
	EnableExternalNameService bool
	service, err := p.dag.EnsureService(meta, intstr.FromInt(int(*port)), p.source, p.EnableExternalNameService)
		return nil, fmt.Errorf("service %q is invalid: %s", meta.Name, err)
	// EnableExternalNameService allows processing of ExternalNameServices
	// This is normally disabled for security reasons.
	// See https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for details.
	EnableExternalNameService bool

			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source, p.EnableExternalNameService)
			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source, p.EnableExternalNameService)
				validCond.AddErrorf(contour_api_v1.ConditionTypeTCPProxyError, "ServiceUnresolvedReference",

	// EnableExternalNameService allows processing of ExternalNameServices
	// This is normally disabled for security reasons.
	// See https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for details.
	EnableExternalNameService bool
		s, err := p.dag.EnsureService(m, port, p.source, p.EnableExternalNameService)
				WithError(contour_api_v1.ConditionTypeTCPProxyError, "ServiceUnresolvedReference", `Spec.TCPProxy unresolved service reference: service "roots/not-found" not found`),
				WithError(contour_api_v1.ConditionTypeTCPProxyError, "ServiceUnresolvedReference", `Spec.TCPProxy unresolved service reference: port "9999" on service "roots/kuard" not matched`),
					Message: "service \"invalid-one\" is invalid: service \"default/invalid-one\" not found, service \"invalid-two\" is invalid: service \"default/invalid-two\" not found",
						Message: "service \"invalid-one\" is invalid: service \"default/invalid-one\" not found, service \"invalid-two\" is invalid: service \"default/invalid-two\" not found",
	"github.com/projectcontour/contour/internal/contour"
	"github.com/projectcontour/contour/internal/dag"
	"github.com/sirupsen/logrus"
	rh, c, done := setup(t, enableExternalNameService(t))

func enableExternalNameService(t *testing.T) func(eh *contour.EventHandler) {
	return func(eh *contour.EventHandler) {

		log := fixture.NewTestLogger(t)
		log.SetLevel(logrus.DebugLevel)

		eh.Builder.Processors = []dag.Processor{
			&dag.IngressProcessor{
				EnableExternalNameService: true,
				FieldLogger:               log.WithField("context", "IngressProcessor"),
			},
			&dag.HTTPProxyProcessor{
				EnableExternalNameService: true,
			},
			&dag.ExtensionServiceProcessor{
				FieldLogger: log.WithField("context", "ExtensionServiceProcessor"),
			},
			&dag.ListenerProcessor{},
		}
	}
}
	// Enable ExternalName processing here because
	// we need to check that host rewrites work in combination
	// with ExternalName.
	rh, c, done := setup(t, enableExternalNameService(t))
	// EnableExternalNameService allows processing of ExternalNameServices
	// Defaults to disabled for security reasons.
	// TODO(youngnick): put a link to the issue and CVE here.
	EnableExternalNameService bool `yaml:"enableExternalNameService,omitempty"`

| enableExternalNameService | boolean | `false` | Enable ExternalName Service processing. Enabling this has security implications. Please see the [advisory](https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc) for more details. |
		proxy, ok := f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
		if !ok {
			t.Fatalf("The HTTPProxy did not become valid, here are the Valid condition's Errors: %s", httpProxyErrors(proxy))
		}
		proxy, ok := f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
		if !ok {
			t.Fatalf("The HTTPProxy did not become valid, here are the Valid condition's Errors: %s", httpProxyErrors(proxy))
		}

func testExternalNameServiceLocalhostInvalid(namespace string) {
	Specify("external name services with localhost are rejected", func() {
		t := f.T()

		f.Fixtures.Echo.Deploy(namespace, "ingress-conformance-echo")

		externalNameService := &corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: namespace,
				Name:      "external-name-service-localhost",
			},
			Spec: corev1.ServiceSpec{
				Type: corev1.ServiceTypeExternalName,
				// The unit tests test just `localhost`, so test another item from that
				// list.
				ExternalName: "localhost.localdomain",
				Ports: []corev1.ServicePort{
					{
						Name: "http",
						Port: 80,
					},
				},
			},
		}
		require.NoError(t, f.Client.Create(context.TODO(), externalNameService))

		p := &contourv1.HTTPProxy{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: namespace,
				Name:      "external-name-proxy",
			},
			Spec: contourv1.HTTPProxySpec{
				VirtualHost: &contourv1.VirtualHost{
					Fqdn: "externalnameservice.projectcontour.io",
				},
				Routes: []contourv1.Route{
					{
						Services: []contourv1.Service{
							{
								Name: externalNameService.Name,
								Port: 80,
							},
						},
						RequestHeadersPolicy: &contourv1.HeadersPolicy{
							Set: []contourv1.HeaderValue{
								{
									Name:  "Host",
									Value: externalNameService.Spec.ExternalName,
								},
							},
						},
					},
				},
			},
		}
		_, ok := f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
		require.Falsef(t, ok, "ExternalName with hostname %s was accepted by Contour.", externalNameService.Spec.ExternalName)
	})
}
	"github.com/davecgh/go-spew/spew"
	f.NamespacedTest("018-external-name-service-insecure", func(namespace string) {
		Context("with ExternalName Services enabled", func() {
			BeforeEach(func() {
				contourConfig.EnableExternalNameService = true
			})
			testExternalNameServiceInsecure(namespace)
		})
	})
	f.NamespacedTest("018-external-name-service-tls", func(namespace string) {
		Context("with ExternalName Services enabled", func() {
			BeforeEach(func() {
				contourConfig.EnableExternalNameService = true
			})
			testExternalNameServiceTLS(namespace)
		})
	})
	f.NamespacedTest("018-external-name-service-localhost", func(namespace string) {
		Context("with ExternalName Services enabled", func() {
			BeforeEach(func() {
				contourConfig.EnableExternalNameService = true
			})
			testExternalNameServiceLocalhostInvalid(namespace)
		})
	})

	if proxy == nil {
		return false
	}

	if len(proxy.Status.Conditions) == 0 {
		return false
	}

	cond := proxy.Status.GetConditionFor("Valid")
	return cond.Status == "True"

}

// httpProxyErrors provides a pretty summary of any Errors on the HTTPProxy Valid condition.
// If there are no errors, the return value will be empty.
func httpProxyErrors(proxy *contourv1.HTTPProxy) string {
	cond := proxy.Status.GetConditionFor("Valid")
	errors := cond.Errors
	if len(errors) > 0 {
		return spew.Sdump(errors)
	}

	return ""