CVE-2021-32783 (Go)

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.

Weakness:
CWE-610: name: Externally Controlled Reference to a Resource in Another Sphere
description: 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

Vulnerable code:
file_path: internal/dag/accessors.go
snippet: func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache) (*Service, error) {
	svc, svcPort, err := cache.LookupService(meta, port)
	if err != nil {
		return nil, err
	}

	if dagSvc := dag.GetService(k8s.NamespacedNameOf(svc), svcPort.Port); dagSvc != nil {
		return dagSvc, nil
	}

	dagSvc := &Service{
		Weighted: WeightedService{
			ServiceName:      svc.Name,
			ServiceNamespace: svc.Namespace,
			ServicePort:      svcPort,
			Weight:           1,
		},
		Protocol:           upstreamProtocol(svc, svcPort),
		MaxConnections:     annotation.MaxConnections(svc),
		MaxPendingRequests: annotation.MaxPendingRequests(svc),
		MaxRequests:        annotation.MaxRequests(svc),
		MaxRetries:         annotation.MaxRetries(svc),
		ExternalName:       externalName(svc),
	}
	return dagSvc, nil
}

Fixed code:
file_path: internal/dag/accessors.go
snippet: func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache, enableExternalNameSvc bool) (*Service, error) {
	svc, svcPort, err := cache.LookupService(meta, port)
	if err != nil {
		return nil, err
	}

	err = validateExternalName(svc, enableExternalNameSvc)
	if err != nil {
		return nil, err
	}

	if dagSvc := dag.GetService(k8s.NamespacedNameOf(svc), svcPort.Port); dagSvc != nil {
		return dagSvc, nil
	}

	dagSvc := &Service{
		Weighted: WeightedService{
			ServiceName:      svc.Name,
			ServiceNamespace: svc.Namespace,
			ServicePort:      svcPort,
			Weight:           1,
		},
		Protocol:           upstreamProtocol(svc, svcPort),
		MaxConnections:     annotation.MaxConnections(svc),
		MaxPendingRequests: annotation.MaxPendingRequests(svc),
		MaxRequests:        annotation.MaxRequests(svc),
		MaxRetries:         annotation.MaxRetries(svc),
		ExternalName:       externalName(svc),
	}
	return dagSvc, nil
}

file_path: internal/dag/accessors.go
snippet: 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
}