CVE-2021-41803 (Go)

HashiCorp Consul's Auto Config RPC uses caller-supplied request fields such as the node name and segment name when interpolating variables into JWT claim assertion expressions. These values are not sufficiently validated before interpolation, allowing malformed names or expression metacharacters to break out of the intended string context or otherwise alter the authorization expression. An attacker who can submit an Auto Config initial configuration request with a valid JWT and crafted node or segment values may bypass the intended claim-to-request binding and reach the auto-configuration provisioning path, potentially obtaining sensitive configuration material such as ACL agent tokens, gossip or TLS configuration, CA roots, or certificates for an unauthorized node or segment.

Weakness:
CWE-862: name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.

Repository: https://github.com/hashicorp/consul

Vulnerable code:
file_path: agent/consul/auto_config_endpoint.go
snippet: func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfigOptions, error) {
	// perform basic JWT Authorization
	identity, err := a.validator.ValidateLogin(context.Background(), req.JWT)
	if err != nil {
		// TODO (autoconf) maybe we should add a more generic permission denied error not tied to the ACL package?
		return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err)
	}

	varMap := map[string]string{
		"node":      req.Node,
		"segment":   req.Segment,
		"partition": req.PartitionOrDefault(),
	}

	for _, raw := range a.claimAssertions {
		// validate and fill any HIL
		filled, err := template.InterpolateHIL(raw, varMap, true)
		if err != nil {
			return AutoConfigOptions{}, fmt.Errorf("Failed to render claim assertion template %q: %w", raw, err)
		}

		evaluator, err := bexpr.CreateEvaluatorForType(filled, nil, identity.SelectableFields)
		if err != nil {
			return AutoConfigOptions{}, fmt.Errorf("Failed to create evaluator for claim assertion %q: %w", filled, err)
		}

		ok, err := evaluator.Evaluate(identity.SelectableFields)
		if err != nil {
			return AutoConfigOptions{}, fmt.Errorf("Failed to execute claim assertion %q: %w", filled, err)
		}

		if !ok {
			return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT claim assertion")
		}
	}

	opts := AutoConfigOptions{
		NodeName:    req.Node,
		SegmentName: req.Segment,
		Partition:   req.Partition,
	}

	if req.CSR != "" {
		csr, id, err := parseAutoConfigCSR(req.CSR)
		if err != nil {
			return AutoConfigOptions{}, err
		}

		if id.Agent != req.Node || !structs.EqualPartitions(id.Partition, req.Partition) {
			return AutoConfigOptions{},
				fmt.Errorf("Spiffe ID agent name (%s) of the certificate signing request is not for the correct node (%s)",
					printNodeName(id.Agent, id.Partition),
					printNodeName(req.Node, req.Partition),
				)
		}

		opts.CSR = csr
		opts.SpiffeID = id
	}

	return opts, nil
}

Fixed code:
file_path: agent/consul/auto_config_endpoint.go
snippet: var invalidSegmentName = regexp.MustCompile("[`'\"\\s]+")

func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfigOptions, error) {
	// perform basic JWT Authorization
	identity, err := a.validator.ValidateLogin(context.Background(), req.JWT)
	if err != nil {
		// TODO (autoconf) maybe we should add a more generic permission denied error not tied to the ACL package?
		return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err)
	}

	// Ensure provided data cannot escape the RHS of a bexpr for security.
	// This is not the cleanest way to prevent this behavior. Ideally, the bexpr would allow us to
	// inject a variable on the RHS for comparison as well, but it would be a complex change to implement
	// that would likely break backwards-compatibility in certain circumstances.
	if dns.InvalidNameRe.MatchString(req.Node) {
		return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "node", req.Node)
	}
	if invalidSegmentName.MatchString(req.Segment) {
		return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "segment", req.Segment)
	}
	if req.Partition != "" && !dns.IsValidLabel(req.Partition) {
		return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "partition", req.Partition)
	}

	// Ensure that every value in this mapping is safe to interpolate before using it.
	varMap := map[string]string{
		"node":      req.Node,
		"segment":   req.Segment,
		"partition": req.PartitionOrDefault(),
	}

	for _, raw := range a.claimAssertions {
		// validate and fill any HIL
		filled, err := template.InterpolateHIL(raw, varMap, true)
		if err != nil {
			return AutoConfigOptions{}, fmt.Errorf("Failed to render claim assertion template %q: %w", raw, err)
		}

		evaluator, err := bexpr.CreateEvaluatorForType(filled, nil, identity.SelectableFields)
		if err != nil {
			return AutoConfigOptions{}, fmt.Errorf("Failed to create evaluator for claim assertion %q: %w", filled, err)
		}

		ok, err := evaluator.Evaluate(identity.SelectableFields)
		if err != nil {
			return AutoConfigOptions{}, fmt.Errorf("Failed to execute claim assertion %q: %w", filled, err)
		}

		if !ok {
			return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT claim assertion")
		}
	}

	opts := AutoConfigOptions{
		NodeName:    req.Node,
		SegmentName: req.Segment,
		Partition:   req.Partition,
	}

	if req.CSR != "" {
		csr, id, err := parseAutoConfigCSR(req.CSR)
		if err != nil {
			return AutoConfigOptions{}, err
		}

		if id.Agent != req.Node || !structs.EqualPartitions(id.Partition, req.Partition) {
			return AutoConfigOptions{},
				fmt.Errorf("Spiffe ID agent name (%s) of the certificate signing request is not for the correct node (%s)",
					printNodeName(id.Agent, id.Partition),
					printNodeName(req.Node, req.Partition),
				)
		}

		opts.CSR = csr
		opts.SpiffeID = id
	}

	return opts, nil
}