VYPR
High severityNVD Advisory· Published Mar 5, 2021· Updated Aug 3, 2024

CVE-2021-27098

CVE-2021-27098

Description

SPIRE Server Legacy Node API allows agents to request unauthorized SPIFFE IDs, leading to certificate issuance with incorrect URI SAN values.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

SPIRE Server Legacy Node API allows agents to request unauthorized SPIFFE IDs, leading to certificate issuance with incorrect URI SAN values.

The vulnerability resides in SPIRE Server's Legacy Node API, specifically in the FetchX509SVID RPC. The API fails to properly validate that the requesting agent is authorized to distribute the requested SPIFFE ID, allowing an already-authenticated agent to obtain a certificate for a SPIFFE ID it should not be able to issue [1][3].

An attacker must possess a valid agent certificate and operate within the same trust domain. By crafting a specially crafted request to the FetchX509SVID RPC, they can receive an X.509 certificate with a URI SAN for an unauthorized SPIFFE ID [1][3]. This bypasses intended authorization controls.

The impact is that the attacker can impersonate another workload within the trust domain, potentially gaining unauthorized access to resources protected by SPIFFE authentication [3]. The fix, introduced in the commit referenced, enforces proper authorization checks so that the agent must be explicitly authorized for the requested SPIFFE ID [2].

The issue has been patched in SPIRE versions 0.8.5, 0.9.4, 0.10.2, 0.11.3, and 0.12.1 [1][3]. No workarounds are available, and users are advised to upgrade to the latest patched version.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/spiffe/spireGo
>= 0.8.1, < 0.8.50.8.5
github.com/spiffe/spireGo
>= 0.9.0, < 0.9.40.9.4
github.com/spiffe/spireGo
>= 0.10.0, < 0.10.20.10.2
github.com/spiffe/spireGo
>= 0.11.0, < 0.11.30.11.3
github.com/spiffe/spireGo
>= 0.12.0, < 0.12.10.12.1

Affected products

2

Patches

1
3c5115b57afc

Merge pull request from GHSA-q7gm-mjrg-44h9

https://github.com/spiffe/spireAndrew HardingMar 4, 2021via ghsa
36 files changed · +854 73
  • cmd/spire-server/cli/run/run.go+8 0 modified
    @@ -87,6 +87,8 @@ type serverConfig struct {
     	ProfilingFreq    int      `hcl:"profiling_freq"`
     	ProfilingNames   []string `hcl:"profiling_names"`
     
    +	AllowUnsafeIDs *bool `hcl:"allow_unsafe_ids"`
    +
     	UnusedKeys []string `hcl:",unusedKeys"`
     }
     
    @@ -471,6 +473,12 @@ func NewServerConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool
     		}
     	}
     
    +	// This is a terrible hack but is just a short-term band-aid.
    +	if c.Server.AllowUnsafeIDs != nil {
    +		sc.Log.Warn("The insecure allow_unsafe_ids configurable will be deprecated in a future release.")
    +		idutil.SetAllowUnsafeIDs(*c.Server.AllowUnsafeIDs)
    +	}
    +
     	return sc, nil
     }
     
    
  • pkg/common/idutil/safety.go+193 0 added
    @@ -0,0 +1,193 @@
    +package idutil
    +
    +import (
    +	"errors"
    +	"fmt"
    +	"net/url"
    +	"path"
    +	"regexp"
    +	"strings"
    +
    +	"github.com/spiffe/go-spiffe/v2/spiffeid"
    +	"github.com/spiffe/spire/proto/spire/types"
    +)
    +
    +var (
    +	rePercentEncodedASCII = regexp.MustCompile(`%[0-7][[:xdigit:]]`)
    +	rePercentEncoded      = regexp.MustCompile(`%[[:xdigit:]][[:xdigit:]]`)
    +
    +	allowUnsafeIDsPolicy bool
    +)
    +
    +func allowUnsafeIDs() bool {
    +	return allowUnsafeIDsPolicy
    +}
    +
    +// SetAllowUnsafeIDs effectively removes all safety checks provided by the
    +// "safety" functions in this source file. It is a switch to allow turning off
    +// the safety valve for deployments that need time to adjust API usage to
    +// conform to the restrictions.
    +func SetAllowUnsafeIDs(allow bool) {
    +	allowUnsafeIDsPolicy = allow
    +}
    +
    +// CheckIDProtoNormalization ensures the the provided ID is properly normalized.
    +func CheckIDProtoNormalization(in *types.SPIFFEID) error {
    +	if allowUnsafeIDs() {
    +		return nil
    +	}
    +	s, err := IDProtoString(in)
    +	if err != nil {
    +		return err
    +	}
    +	return CheckIDStringNormalization(s)
    +}
    +
    +// CheckIDStringNormalization ensures the the provided ID is properly normalized.
    +func CheckIDStringNormalization(id string) error {
    +	if allowUnsafeIDs() {
    +		return nil
    +	}
    +
    +	// Parse the URL. This will unescape the percent-encoded characters. If
    +	// there are invalid percent-encoded characters, this function will fail.
    +	u, err := url.Parse(id)
    +	if err != nil {
    +		return err
    +	}
    +
    +	return CheckIDURLNormalization(u)
    +}
    +
    +// CheckIDURLNormalization returns if a URL is normalized or not. It relies on
    +// behavior and fields populated by url.Parse(). DO NOT call it with a URL that
    +// has not gone through url.Parse().
    +func CheckIDURLNormalization(u *url.URL) error {
    +	if allowUnsafeIDs() {
    +		return nil
    +	}
    +
    +	// Rule out percent-encoded ASCII
    +	if rePercentEncodedASCII.MatchString(u.EscapedPath()) {
    +		return errors.New("path cannot contain percent-encoded ASCII characters")
    +	}
    +
    +	// At this point, if RawPath is set, then the path contains non-ASCII
    +	// characters, since percent-encoded ASCII was ruled out above. Ensure
    +	// that there is no percent-encoded characters, since that would imply
    +	// a mix-n-match of utf-8 and percent-encoded utf-8, which we want to
    +	// reject since it wouldn't be normal to have this kind of path and it
    +	// likely indicates either 1) a bug, or 2) malicious intent.
    +	if u.RawPath != "" && rePercentEncoded.MatchString(u.RawPath) {
    +		return errors.New("path cannot contain both non-ASCII and percent-encoded characters")
    +	}
    +
    +	// Check the scheme and host
    +	switch {
    +	case u.Scheme != "spiffe":
    +		return errors.New("scheme must be 'spiffe'")
    +	case strings.ToLower(u.Host) != u.Host:
    +		return errors.New("trust domain name must be lowercase")
    +	}
    +
    +	// Check the path
    +	switch {
    +	case u.Path == "":
    +	case u.Path[len(u.Path)-1] == '/':
    +		return errors.New("path cannot have a trailing slash")
    +	case u.Path != path.Clean(u.Path):
    +		return errors.New("path cannot contain empty, '.', or '..' segments")
    +	}
    +
    +	return nil
    +}
    +
    +// IDProtoString constructs a URL string for the given ID protobuf. It does
    +// not interpret the contents of the trust domain or path with the exception
    +// of adding a leading slash on the path where necessary.
    +func IDProtoString(id *types.SPIFFEID) (string, error) {
    +	if id.TrustDomain == "" {
    +		return "", errors.New("trust domain is empty")
    +	}
    +	return "spiffe://" + id.TrustDomain + ensureLeadingSlash(id.Path), nil
    +}
    +
    +// IDProtoFromString parses a SPIFFE ID string into the raw ID proto components.
    +// It does not attempt to escape/unescape any portion of the ID.
    +func IDProtoFromString(id string) (*types.SPIFFEID, error) {
    +	trimmed := strings.TrimPrefix(id, "spiffe://")
    +	if trimmed == id {
    +		return nil, errors.New(`scheme must be "spiffe://"`)
    +	}
    +	parts := strings.SplitN(trimmed, "/", 2)
    +	td := parts[0]
    +	if len(td) == 0 {
    +		return nil, errors.New("trust domain is empty")
    +	}
    +	path := ""
    +	if len(parts) > 1 {
    +		path = "/" + parts[1]
    +	}
    +	return &types.SPIFFEID{
    +		TrustDomain: td,
    +		Path:        path,
    +	}, nil
    +}
    +
    +// CheckAgentIDStringNormalization ensures the provided agent ID string is
    +// properly normalized. It also ensures it is not a server ID.
    +func CheckAgentIDStringNormalization(agentID string) error {
    +	if allowUnsafeIDs() {
    +		return nil
    +	}
    +
    +	// Parse the URL. This will unescape the percent-encoded characters. If
    +	// there are invalid percent-encoded characters, this function will fail.
    +	u, err := url.Parse(agentID)
    +	if err != nil {
    +		return err
    +	}
    +
    +	if err := CheckIDURLNormalization(u); err != nil {
    +		return err
    +	}
    +
    +	// We want to do more than this but backcompat compels us to not too. We'll
    +	// get more aggressive in the future.
    +	if u.Path == ServerIDPath {
    +		return errors.New("server ID is not allowed for agents")
    +	}
    +
    +	return nil
    +}
    +
    +// IDFromProto returns SPIFFE ID from the proto representation
    +func IDFromProto(id *types.SPIFFEID) (spiffeid.ID, error) {
    +	if allowUnsafeIDs() {
    +		return spiffeid.New(id.TrustDomain, id.Path)
    +	}
    +	s, err := IDProtoString(id)
    +	if err != nil {
    +		return spiffeid.ID{}, err
    +	}
    +	return spiffeid.FromString(s)
    +}
    +
    +// FormatPath formats a path string. The function ensures a leading slash is
    +// present.
    +func FormatPath(format string, args ...interface{}) string {
    +	return ensureLeadingSlash(fmt.Sprintf(format, args...))
    +}
    +
    +// JoinPathSegments escapes path segments and joins them together. The
    +// function also ensures a leading slash is present.
    +func JoinPathSegments(segments ...string) string {
    +	return ensureLeadingSlash(strings.Join(segments, "/"))
    +}
    +
    +func ensureLeadingSlash(p string) string {
    +	if p != "" && p[0] != '/' {
    +		p = "/" + p
    +	}
    +	return p
    +}
    
  • pkg/common/idutil/safety_test.go+268 0 added
    @@ -0,0 +1,268 @@
    +package idutil
    +
    +import (
    +	"net/url"
    +	"strings"
    +	"testing"
    +
    +	"github.com/spiffe/spire/proto/spire/types"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestCheckIDURLNormalization(t *testing.T) {
    +	assertGood := func(id string) {
    +		u, err := url.Parse(id)
    +		require.NoError(t, err)
    +		assert.NoError(t, CheckIDURLNormalization(u), "%s should have passed", id)
    +	}
    +	assertBad := func(id string, expectedErr string) {
    +		u, err := url.Parse(id)
    +		if err != nil {
    +			assert.EqualError(t, err, expectedErr, "parsing %s should have failed", id)
    +			return
    +		}
    +		assert.EqualError(t, CheckIDURLNormalization(u), expectedErr, "%s should have failed", id)
    +	}
    +
    +	// Assert the scheme is spiffe
    +	assertBad("sparfe://example.org/workload",
    +		"scheme must be 'spiffe'")
    +
    +	testCommonCheckIDNormalization(assertGood, assertBad)
    +}
    +
    +func TestCheckIDStringNormalization(t *testing.T) {
    +	assertGood := func(id string) {
    +		assert.NoError(t, CheckIDStringNormalization(id), "%s should have passed", id)
    +	}
    +	assertBad := func(id string, expectedErr string) {
    +		assert.EqualError(t, CheckIDStringNormalization(id), expectedErr, "%s should have failed", id)
    +	}
    +
    +	// Assert the scheme is spiffe
    +	assertBad("sparfe://example.org/workload",
    +		"scheme must be 'spiffe'")
    +
    +	// Test the common normalization cases
    +	testCommonCheckIDNormalization(assertGood, assertBad)
    +}
    +
    +func TestCheckIDProtoNormalization(t *testing.T) {
    +	protoFromString := func(t *testing.T, id string) *types.SPIFFEID {
    +		// Rough parsing. We know the shape of the incoming ID's so this is
    +		// ok. We don't want to use url.Parse() here because it will trip
    +		// up some of the tests.
    +		schemeIndex := strings.Index(id, "://")
    +		require.GreaterOrEqual(t, schemeIndex, 0)
    +		rest := id[schemeIndex+3:]
    +		pathIndex := strings.IndexByte(rest, '/')
    +
    +		out := &types.SPIFFEID{TrustDomain: rest}
    +		if pathIndex >= 0 {
    +			out.TrustDomain = rest[:pathIndex]
    +			out.Path = rest[pathIndex:]
    +		}
    +		return out
    +	}
    +	assertGood := func(id string) {
    +		assert.NoError(t, CheckIDProtoNormalization(protoFromString(t, id)), "%s should have passed", id)
    +	}
    +	assertBad := func(id string, expectedErr string) {
    +		assert.EqualError(t, CheckIDProtoNormalization(protoFromString(t, id)), expectedErr, "%s should have failed", id)
    +	}
    +
    +	// Since the ID proto doesn't include the scheme, the following
    +	// test is irrelevant but is included here for parity with the other
    +	// Check* tests.
    +	assertGood("sparfe://example.org/workload")
    +
    +	// Assert that the path is auto-prefixed with "/" when considering
    +	// normalization
    +	assert.NoError(t, CheckIDProtoNormalization(&types.SPIFFEID{
    +		TrustDomain: "example.org",
    +		Path:        "workload",
    +	}))
    +
    +	// Test the common normalization cases
    +	testCommonCheckIDNormalization(assertGood, assertBad)
    +}
    +
    +func TestCheckAgentIDStringNormalization(t *testing.T) {
    +	assertGood := func(id string) {
    +		assert.NoError(t, CheckAgentIDStringNormalization(id), "%s should have passed", id)
    +	}
    +	assertBad := func(id string, expectedErr string) {
    +		assert.EqualError(t, CheckAgentIDStringNormalization(id), expectedErr, "%s should have failed", id)
    +	}
    +
    +	// Assert the scheme is spiffe
    +	assertBad("sparfe://example.org/workload",
    +		"scheme must be 'spiffe'")
    +
    +	// Agent ID cannot be the server ID
    +	assertBad("spiffe://example.org/spire/server",
    +		"server ID is not allowed for agents")
    +
    +	// Test the common normalization cases
    +	testCommonCheckIDNormalization(assertGood, assertBad)
    +}
    +
    +func testCommonCheckIDNormalization(assertGood func(string), assertBad func(string, string)) {
    +	assertGood("spiffe://example.org")
    +	assertGood("spiffe://example.org/workload")
    +	assertGood("spiffe://example.org/workload/%E4%B8%96%E7%95%8C")
    +	assertGood("sPiFfE://example.org/workload")
    +	assertGood("spiffe://世界/workload")
    +	assertGood("spiffe://example.org/世界")
    +	assertGood("spiffe://%E4%B8%96%E7%95%8C/workload")
    +
    +	assertBad("spiffe://%45example.org/workload",
    +		`parse "spiffe://%45example.org/workload": invalid URL escape "%45"`)
    +	assertBad("spiffe://example.org/世界/%E4%B8%96%E7%95%8C",
    +		`path cannot contain both non-ASCII and percent-encoded characters`)
    +	assertBad("spiffe://example.org/",
    +		"path cannot have a trailing slash")
    +	assertBad("spiffe://example.org/workload/",
    +		"path cannot have a trailing slash")
    +	assertBad("spiffe://eXaMplE.org/workload",
    +		"trust domain name must be lowercase")
    +	assertBad("spiffe://example.org//workload",
    +		"path cannot contain empty, '.', or '..' segments")
    +	assertBad("spiffe://example.org///workload",
    +		"path cannot contain empty, '.', or '..' segments")
    +	assertBad("spiffe://example.org/./workload",
    +		"path cannot contain empty, '.', or '..' segments")
    +	assertBad("spiffe://example.org/workload/../workload2",
    +		"path cannot contain empty, '.', or '..' segments")
    +	assertBad("spiffe://example.org/workload/%2e%2e/workload2",
    +		"path cannot contain percent-encoded ASCII characters")
    +	assertBad("spiffe://example.org/workload/%252e",
    +		"path cannot contain percent-encoded ASCII characters")
    +	assertBad("spiffe://example.org/workload/%23",
    +		"path cannot contain percent-encoded ASCII characters")
    +	assertBad("spiffe://example.org/workload/%00",
    +		"path cannot contain percent-encoded ASCII characters")
    +	assertBad("spiffe://example.org/%2z",
    +		`parse "spiffe://example.org/%2z": invalid URL escape "%2z"`)
    +	assertBad("spiffe://example.org/workload/"+url.PathEscape("%E4%B8%96%E7%95%8C"),
    +		"path cannot contain percent-encoded ASCII characters")
    +
    +	// Now test that the function responds favorably if the checks are
    +	// disabled via the flag.
    +	SetAllowUnsafeIDs(true)
    +	defer SetAllowUnsafeIDs(false)
    +	assertGood("spiffe://example.org/")
    +}
    +
    +func TestIDProtoString(t *testing.T) {
    +	assert := assert.New(t)
    +
    +	id, err := IDProtoString(&types.SPIFFEID{})
    +	assert.EqualError(err, "trust domain is empty")
    +	assert.Empty(id)
    +
    +	id, err = IDProtoString(&types.SPIFFEID{TrustDomain: "example.org"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org", id)
    +
    +	id, err = IDProtoString(&types.SPIFFEID{TrustDomain: "example.org", Path: "/"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/", id)
    +
    +	id, err = IDProtoString(&types.SPIFFEID{TrustDomain: "example.org", Path: "workload"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/workload", id)
    +
    +	id, err = IDProtoString(&types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/workload", id)
    +
    +	id, err = IDProtoString(&types.SPIFFEID{TrustDomain: "example.org", Path: "/workload/foo"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/workload/foo", id)
    +}
    +
    +func TestIDProtoFromString(t *testing.T) {
    +	assert := assert.New(t)
    +
    +	id, err := IDProtoFromString("other://whocares")
    +	assert.EqualError(err, `scheme must be "spiffe://"`)
    +	assert.Nil(id)
    +
    +	id, err = IDProtoFromString("spiffe://")
    +	assert.EqualError(err, "trust domain is empty")
    +	assert.Nil(id)
    +
    +	id, err = IDProtoFromString("spiffe://example.org")
    +	assert.NoError(err)
    +	assert.Equal(&types.SPIFFEID{TrustDomain: "example.org"}, id)
    +
    +	id, err = IDProtoFromString("spiffe://example.org/")
    +	assert.NoError(err)
    +	assert.Equal(&types.SPIFFEID{TrustDomain: "example.org", Path: "/"}, id)
    +
    +	id, err = IDProtoFromString("spiffe://example.org/workload")
    +	assert.NoError(err)
    +	assert.Equal(&types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"}, id)
    +
    +	id, err = IDProtoFromString("spiffe://example.org/workload/foo")
    +	assert.NoError(err)
    +	assert.Equal(&types.SPIFFEID{TrustDomain: "example.org", Path: "/workload/foo"}, id)
    +}
    +
    +func TestIDFromProto(t *testing.T) {
    +	assert := assert.New(t)
    +
    +	id, err := IDFromProto(&types.SPIFFEID{})
    +	assert.EqualError(err, "trust domain is empty")
    +	assert.Empty(id)
    +
    +	id, err = IDFromProto(&types.SPIFFEID{TrustDomain: "example.org"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org", id.String())
    +
    +	id, err = IDFromProto(&types.SPIFFEID{TrustDomain: "example.org", Path: "/"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/", id.String())
    +
    +	id, err = IDFromProto(&types.SPIFFEID{TrustDomain: "example.org", Path: "workload"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/workload", id.String())
    +
    +	id, err = IDFromProto(&types.SPIFFEID{TrustDomain: "example.org", Path: "/workload"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/workload", id.String())
    +
    +	id, err = IDFromProto(&types.SPIFFEID{TrustDomain: "example.org", Path: "/workload/%41%42%43"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/workload/ABC", id.String())
    +
    +	SetAllowUnsafeIDs(true)
    +	defer SetAllowUnsafeIDs(false)
    +
    +	// When unsafe IDs are allowed, this will not percent encoding properly
    +	// which restores the original behavior of the API.
    +	id, err = IDFromProto(&types.SPIFFEID{TrustDomain: "example.org", Path: "/workload/%41%42%43"})
    +	assert.NoError(err)
    +	assert.Equal("spiffe://example.org/workload/%2541%2542%2543", id.String())
    +}
    +
    +func TestJoinPathSegments(t *testing.T) {
    +	assert := assert.New(t)
    +
    +	assert.Equal("", JoinPathSegments())
    +	assert.Equal("/foo", JoinPathSegments("foo"))
    +	assert.Equal("/foo", JoinPathSegments("/foo"))
    +	assert.Equal("/foo/世界", JoinPathSegments("foo", "世界"))
    +}
    +
    +func TestFormatPath(t *testing.T) {
    +	assert := assert.New(t)
    +
    +	assert.Equal("", FormatPath(""))
    +	assert.Equal("/", FormatPath("/"))
    +	assert.Equal("/foo", FormatPath("%s", "foo"))
    +	assert.Equal("/foo", FormatPath("/%s", "foo"))
    +	assert.Equal("/foo//世界", FormatPath("%s//%s", "foo", "世界"))
    +}
    
  • pkg/common/idutil/spiffeid.go+1 2 modified
    @@ -4,7 +4,6 @@ import (
     	"errors"
     	"fmt"
     	"net/url"
    -	"path"
     	"strings"
     
     	"github.com/spiffe/go-spiffe/v2/spiffeid"
    @@ -332,7 +331,7 @@ func AgentURI(trustDomain, p string) *url.URL {
     	return &url.URL{
     		Scheme: "spiffe",
     		Host:   trustDomain,
    -		Path:   path.Join("spire", "agent", p),
    +		Path:   "/spire/agent" + ensureLeadingSlash(p),
     	}
     }
     
    
  • pkg/common/telemetry/names.go+3 0 modified
    @@ -403,6 +403,9 @@ const (
     	// with other tags to add clarity
     	FederatedBundle = "federated_bundle"
     
    +	// Ignored tags something as ignored
    +	Ignored = "ignored"
    +
     	// JoinToken functionality related to a join token; should be used
     	// with other tags to add clarity
     	JoinToken = "join_token"
    
  • pkg/common/telemetry/server/server.go+9 0 added
    @@ -0,0 +1,9 @@
    +package server
    +
    +import "github.com/spiffe/spire/pkg/common/telemetry"
    +
    +// SetEntryIgnoredGauge emits a gauge with the number of entries that will
    +// be ignored in the entry cache.
    +func SetEntryIgnoredGauge(m telemetry.Metrics, ignored int) {
    +	m.SetGauge([]string{telemetry.Entry, telemetry.Ignored}, float32(ignored))
    +}
    
  • pkg/server/api/agent/v1/service.go+10 1 modified
    @@ -13,6 +13,7 @@ import (
     	"github.com/gofrs/uuid"
     	"github.com/sirupsen/logrus"
     	"github.com/spiffe/go-spiffe/v2/spiffeid"
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/pkg/common/nodeutil"
     	"github.com/spiffe/spire/pkg/common/telemetry"
     	"github.com/spiffe/spire/pkg/common/x509util"
    @@ -254,11 +255,16 @@ func (s *Service) AttestAgent(stream agent.Agent_AttestAgentServer) error {
     	}
     
     	agentID := attestResp.AgentId
    +	log = log.WithField(telemetry.AgentID, agentID)
    +
    +	if err := idutil.CheckAgentIDStringNormalization(agentID); err != nil {
    +		return api.MakeErr(log, codes.Internal, "agent ID is malformed", err)
    +	}
    +
     	agentSpiffeID, err := spiffeid.FromString(agentID)
     	if err != nil {
     		return api.MakeErr(log, codes.Internal, "invalid agent id", err)
     	}
    -	log = log.WithField(telemetry.AgentID, agentID)
     
     	// fetch the agent/node to check if it was already attested or banned
     	attestedNode, err := s.ds.FetchAttestedNode(ctx, &datastore.FetchAttestedNodeRequest{
    @@ -395,6 +401,9 @@ func (s *Service) CreateJoinToken(ctx context.Context, req *agent.CreateJoinToke
     		if err != nil {
     			return nil, api.MakeErr(log, codes.InvalidArgument, "invalid agent ID", err)
     		}
    +		if err := idutil.CheckIDProtoNormalization(req.AgentId); err != nil {
    +			return nil, api.MakeErr(log, codes.InvalidArgument, "agent ID is malformed", err)
    +		}
     		log.WithField(telemetry.SPIFFEID, agentID.String())
     	}
     
    
  • pkg/server/api/agent/v1/service_test.go+4 4 modified
    @@ -445,13 +445,13 @@ func TestBanAgent(t *testing.T) {
     				TrustDomain: "ex ample.org",
     			},
     			expectCode: codes.InvalidArgument,
    -			expectMsg:  `invalid agent ID: spiffeid: unable to parse: parse "spiffe://ex ample.org": invalid character " " in host name`,
    +			expectMsg:  `invalid agent ID: spiffeid: unable to parse: parse "spiffe://ex ample.org/spire/agent/agent-1": invalid character " " in host name`,
     			expectLogs: []spiretest.LogEntry{
     				{
     					Level:   logrus.ErrorLevel,
     					Message: "Invalid argument: invalid agent ID",
     					Data: logrus.Fields{
    -						logrus.ErrorKey: `spiffeid: unable to parse: parse "spiffe://ex ample.org": invalid character " " in host name`,
    +						logrus.ErrorKey: `spiffeid: unable to parse: parse "spiffe://ex ample.org/spire/agent/agent-1": invalid character " " in host name`,
     					},
     				},
     			},
    @@ -641,12 +641,12 @@ func TestDeleteAgent(t *testing.T) {
     					Level:   logrus.ErrorLevel,
     					Message: "Invalid argument: invalid agent ID",
     					Data: logrus.Fields{
    -						logrus.ErrorKey: "spiffeid: trust domain is empty",
    +						logrus.ErrorKey: "trust domain is empty",
     					},
     				},
     			},
     			code: codes.InvalidArgument,
    -			err:  "invalid agent ID: spiffeid: trust domain is empty",
    +			err:  "invalid agent ID: trust domain is empty",
     			req: &agentpb.DeleteAgentRequest{
     				Id: &types.SPIFFEID{
     					TrustDomain: "",
    
  • pkg/server/api/entry.go+7 0 modified
    @@ -5,6 +5,7 @@ import (
     	"fmt"
     
     	"github.com/spiffe/go-spiffe/v2/spiffeid"
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/pkg/common/protoutil"
     	"github.com/spiffe/spire/pkg/common/x509util"
     	"github.com/spiffe/spire/proto/spire/common"
    @@ -93,6 +94,9 @@ func ProtoToRegistrationEntryWithMask(td spiffeid.TrustDomain, e *types.Entry, m
     			return nil, fmt.Errorf("invalid parent ID: %v", err)
     		}
     		parentIDString = parentID.String()
    +		if err := idutil.CheckIDProtoNormalization(e.ParentId); err != nil {
    +			return nil, fmt.Errorf("parent ID is malformed: %v", err)
    +		}
     	}
     
     	var spiffeIDString string
    @@ -102,6 +106,9 @@ func ProtoToRegistrationEntryWithMask(td spiffeid.TrustDomain, e *types.Entry, m
     			return nil, fmt.Errorf("invalid spiffe ID: %v", err)
     		}
     		spiffeIDString = spiffeID.String()
    +		if err := idutil.CheckIDProtoNormalization(e.SpiffeId); err != nil {
    +			return nil, fmt.Errorf("spiffe ID is malformed: %v", err)
    +		}
     	}
     
     	var admin bool
    
  • pkg/server/api/entry/v1/service_test.go+15 18 modified
    @@ -44,8 +44,7 @@ func TestListEntries(t *testing.T) {
     	protoChildID := api.ProtoFromID(childID)
     	protoSecondChildID := api.ProtoFromID(secondChildID)
     	badID := &types.SPIFFEID{
    -		TrustDomain: "http://example.org",
    -		Path:        "/bad",
    +		Path: "/bad",
     	}
     
     	childRegEntry := &common.RegistrationEntry{
    @@ -222,7 +221,7 @@ func TestListEntries(t *testing.T) {
     		},
     		{
     			name:   "bad parent ID filter",
    -			err:    "malformed parent ID filter: spiffeid: invalid scheme",
    +			err:    "malformed parent ID filter: trust domain is empty",
     			code:   codes.InvalidArgument,
     			logMsg: "Invalid argument: malformed parent ID filter",
     			request: &entrypb.ListEntriesRequest{
    @@ -233,7 +232,7 @@ func TestListEntries(t *testing.T) {
     		},
     		{
     			name:   "bad SPIFFE ID filter",
    -			err:    "malformed SPIFFE ID filter: spiffeid: invalid scheme",
    +			err:    "malformed SPIFFE ID filter: trust domain is empty",
     			code:   codes.InvalidArgument,
     			logMsg: "Invalid argument: malformed SPIFFE ID filter",
     			request: &entrypb.ListEntriesRequest{
    @@ -731,7 +730,7 @@ func TestBatchCreateEntry(t *testing.T) {
     				{
     					Status: &types.Status{
     						Code:    int32(codes.InvalidArgument),
    -						Message: "failed to convert entry: invalid parent ID: spiffeid: trust domain is empty",
    +						Message: "failed to convert entry: invalid parent ID: trust domain is empty",
     					},
     				},
     			},
    @@ -740,7 +739,7 @@ func TestBatchCreateEntry(t *testing.T) {
     					Level:   logrus.ErrorLevel,
     					Message: "Invalid argument: failed to convert entry",
     					Data: logrus.Fields{
    -						logrus.ErrorKey: "invalid parent ID: spiffeid: trust domain is empty",
    +						logrus.ErrorKey: "invalid parent ID: trust domain is empty",
     					},
     				},
     			},
    @@ -1322,8 +1321,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     			},
     			updateEntries: []*types.Entry{
     				{
    -					// Trust domain will be normalized to lower case
    -					ParentId: &types.SPIFFEID{TrustDomain: "EXAMPLE.org", Path: "/parentUpdated"},
    +					ParentId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/parentUpdated"},
     				},
     			},
     			expectDsEntries: func(id string) []*types.Entry {
    @@ -1352,8 +1350,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     			},
     			updateEntries: []*types.Entry{
     				{
    -					// Trust domain will be normalized to lower case
    -					SpiffeId: &types.SPIFFEID{TrustDomain: "EXAMPLE.org", Path: "/workloadUpdated"},
    +					SpiffeId: &types.SPIFFEID{TrustDomain: "example.org", Path: "/workloadUpdated"},
     				},
     			},
     			expectDsEntries: func(id string) []*types.Entry {
    @@ -1664,7 +1661,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     			expectResults: []*entrypb.BatchUpdateEntryResponse_Result{
     				{
     					Status: &types.Status{Code: int32(codes.InvalidArgument),
    -						Message: "failed to convert entry: invalid spiffe ID: spiffeid: trust domain is empty"},
    +						Message: "failed to convert entry: invalid spiffe ID: trust domain is empty"},
     				},
     			},
     			expectLogs: func(m map[string]string) []spiretest.LogEntry {
    @@ -1674,7 +1671,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     						Message: "Invalid argument: failed to convert entry",
     						Data: logrus.Fields{
     							telemetry.RegistrationID: m[entry1SpiffeID.Path],
    -							logrus.ErrorKey:          "invalid spiffe ID: spiffeid: trust domain is empty",
    +							logrus.ErrorKey:          "invalid spiffe ID: trust domain is empty",
     						},
     					},
     				}
    @@ -1694,7 +1691,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     			expectResults: []*entrypb.BatchUpdateEntryResponse_Result{
     				{
     					Status: &types.Status{Code: int32(codes.InvalidArgument),
    -						Message: "failed to convert entry: invalid parent ID: spiffeid: trust domain is empty"},
    +						Message: "failed to convert entry: invalid parent ID: trust domain is empty"},
     				},
     			},
     			expectLogs: func(m map[string]string) []spiretest.LogEntry {
    @@ -1704,7 +1701,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     						Message: "Invalid argument: failed to convert entry",
     						Data: logrus.Fields{
     							telemetry.RegistrationID: m[entry1SpiffeID.Path],
    -							logrus.ErrorKey:          "invalid parent ID: spiffeid: trust domain is empty",
    +							logrus.ErrorKey:          "invalid parent ID: trust domain is empty",
     						},
     					},
     				}
    @@ -1724,7 +1721,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     			expectResults: []*entrypb.BatchUpdateEntryResponse_Result{
     				{
     					Status: &types.Status{Code: int32(codes.InvalidArgument),
    -						Message: "failed to convert entry: invalid parent ID: spiffeid: trust domain is empty"},
    +						Message: "failed to convert entry: invalid parent ID: trust domain is empty"},
     				},
     			},
     			expectLogs: func(m map[string]string) []spiretest.LogEntry {
    @@ -1733,7 +1730,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     						Level:   logrus.ErrorLevel,
     						Message: "Invalid argument: failed to convert entry",
     						Data: logrus.Fields{
    -							"error":                  "invalid parent ID: spiffeid: trust domain is empty",
    +							"error":                  "invalid parent ID: trust domain is empty",
     							telemetry.RegistrationID: m[entry1SpiffeID.Path],
     						},
     					},
    @@ -1754,7 +1751,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     			expectResults: []*entrypb.BatchUpdateEntryResponse_Result{
     				{
     					Status: &types.Status{Code: int32(codes.InvalidArgument),
    -						Message: "failed to convert entry: invalid spiffe ID: spiffeid: trust domain is empty"},
    +						Message: "failed to convert entry: invalid spiffe ID: trust domain is empty"},
     				},
     			},
     			expectLogs: func(m map[string]string) []spiretest.LogEntry {
    @@ -1763,7 +1760,7 @@ func TestBatchUpdateEntry(t *testing.T) {
     						Level:   logrus.ErrorLevel,
     						Message: "Invalid argument: failed to convert entry",
     						Data: logrus.Fields{
    -							"error":                  "invalid spiffe ID: spiffeid: trust domain is empty",
    +							"error":                  "invalid spiffe ID: trust domain is empty",
     							telemetry.RegistrationID: m[entry1SpiffeID.Path],
     						},
     					},
    
  • pkg/server/api/id.go+1 1 modified
    @@ -91,5 +91,5 @@ func idFromProto(protoID *types.SPIFFEID) (spiffeid.ID, error) {
     	if protoID == nil {
     		return spiffeid.ID{}, errors.New("request must specify SPIFFE ID")
     	}
    -	return spiffeid.New(protoID.TrustDomain, protoID.Path)
    +	return idutil.IDFromProto(protoID)
     }
    
  • pkg/server/api/id_test.go+1 1 modified
    @@ -33,7 +33,7 @@ func TestIDFromProto(t *testing.T) {
     		{
     			name:        "missing trust domain",
     			spiffeID:    &types.SPIFFEID{Path: "/workload"},
    -			expectedErr: "spiffeid: trust domain is empty",
    +			expectedErr: "trust domain is empty",
     		},
     		{
     			name:        "wrong trust domain",
    
  • pkg/server/api/middleware/caller.go+8 1 modified
    @@ -4,6 +4,7 @@ import (
     	"context"
     
     	"github.com/spiffe/go-spiffe/v2/spiffeid"
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/pkg/server/api/rpccontext"
     	"google.golang.org/grpc/codes"
     	"google.golang.org/grpc/credentials"
    @@ -57,7 +58,13 @@ func tcpCallerContextFromPeer(ctx context.Context, p *peer.Peer) (context.Contex
     		return nil, status.Error(codes.Unauthenticated, "client certificate has more than one URI SAN")
     	}
     
    -	id, err := spiffeid.FromURI(uris[0])
    +	uri := uris[0]
    +
    +	if err := idutil.CheckIDURLNormalization(uri); err != nil {
    +		return nil, status.Errorf(codes.Unauthenticated, "client certificate has a malformed URI SAN: %v", err)
    +	}
    +
    +	id, err := spiffeid.FromURI(uri)
     	if err != nil {
     		return nil, status.Errorf(codes.Unauthenticated, "client certificate has a malformed URI SAN: %v", err)
     	}
    
  • pkg/server/api/middleware/caller_test.go+1 1 modified
    @@ -162,7 +162,7 @@ func TestCallerContextFromContext(t *testing.T) {
     			name:       "mtls peer with malformed URI SAN",
     			peer:       mtlsPeerMalformedURISAN,
     			expectCode: codes.Unauthenticated,
    -			expectMsg:  "client certificate has a malformed URI SAN: spiffeid: invalid scheme",
    +			expectMsg:  "client certificate has a malformed URI SAN: scheme must be 'spiffe'",
     		},
     	} {
     		tt := tt
    
  • pkg/server/api/svid/v1/service.go+9 0 modified
    @@ -7,6 +7,7 @@ import (
     
     	"github.com/sirupsen/logrus"
     	"github.com/spiffe/go-spiffe/v2/spiffeid"
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/pkg/common/jwtsvid"
     	"github.com/spiffe/spire/pkg/common/telemetry"
     	"github.com/spiffe/spire/pkg/common/x509util"
    @@ -87,6 +88,10 @@ func (s *Service) MintX509SVID(ctx context.Context, req *svid.MintX509SVIDReques
     		return nil, api.MakeErr(log, codes.InvalidArgument, "CSR URI SAN is invalid", err)
     	}
     
    +	if err := idutil.CheckIDURLNormalization(csr.URIs[0]); err != nil {
    +		return nil, api.MakeErr(log, codes.InvalidArgument, "CSR URI SAN is malformed", err)
    +	}
    +
     	for _, dnsName := range csr.DNSNames {
     		if err := x509util.ValidateDNS(dnsName); err != nil {
     			return nil, api.MakeErr(log, codes.InvalidArgument, "CSR DNS name is not valid", err)
    @@ -246,6 +251,10 @@ func (s *Service) mintJWTSVID(ctx context.Context, protoID *types.SPIFFEID, audi
     		return nil, api.MakeErr(log, codes.InvalidArgument, "invalid SPIFFE ID", err)
     	}
     
    +	if err := idutil.CheckIDProtoNormalization(protoID); err != nil {
    +		return nil, api.MakeErr(log, codes.InvalidArgument, "spiffe ID is malformed", err)
    +	}
    +
     	log = log.WithField(telemetry.SPIFFEID, id.String())
     
     	if len(audience) == 0 {
    
  • pkg/server/api/svid/v1/service_test.go+2 2 modified
    @@ -316,7 +316,7 @@ func TestServiceMintJWTSVID(t *testing.T) {
     			code:     codes.InvalidArgument,
     			audience: []string{"AUDIENCE"},
     			id:       spiffeid.ID{},
    -			err:      "invalid SPIFFE ID: spiffeid: trust domain is empty",
    +			err:      "invalid SPIFFE ID: trust domain is empty",
     			logMsg:   "Invalid argument: invalid SPIFFE ID",
     		},
     		{
    @@ -442,7 +442,7 @@ func TestServiceNewJWTSVID(t *testing.T) {
     			code:     codes.InvalidArgument,
     			audience: []string{"AUDIENCE"},
     			entry:    invalidEntry,
    -			err:      "invalid SPIFFE ID: spiffeid: trust domain is empty",
    +			err:      "invalid SPIFFE ID: trust domain is empty",
     			logMsg:   "Invalid argument: invalid SPIFFE ID",
     		},
     		{
    
  • pkg/server/cache/entrycache/fullcache_ds.go+21 0 modified
    @@ -5,8 +5,10 @@ import (
     	"time"
     
     	"github.com/spiffe/go-spiffe/v2/spiffeid"
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/pkg/server/api"
     	"github.com/spiffe/spire/pkg/server/plugin/datastore"
    +	"github.com/spiffe/spire/proto/spire/common"
     	"github.com/spiffe/spire/proto/spire/types"
     	"google.golang.org/protobuf/types/known/timestamppb"
     )
    @@ -49,6 +51,8 @@ func (it *entryIteratorDS) Next(ctx context.Context) bool {
     			return false
     		}
     
    +		resp.Entries = it.filterEntries(resp.Entries)
    +
     		it.next = 0
     		it.entries, err = api.RegistrationEntriesToProto(resp.Entries)
     		if err != nil {
    @@ -63,6 +67,23 @@ func (it *entryIteratorDS) Next(ctx context.Context) bool {
     	return true
     }
     
    +func (it *entryIteratorDS) filterEntries(in []*common.RegistrationEntry) []*common.RegistrationEntry {
    +	out := make([]*common.RegistrationEntry, 0, len(in))
    +	for _, entry := range in {
    +		// Filter out entries with invalid SPIFFE IDs. Operators are notified
    +		// that they are ignored on server startup (see
    +		// pkg/server/scanentries.go)
    +		if err := idutil.CheckIDStringNormalization(entry.SpiffeId); err != nil {
    +			continue
    +		}
    +		if err := idutil.CheckIDStringNormalization(entry.ParentId); err != nil {
    +			continue
    +		}
    +		out = append(out, entry)
    +	}
    +	return out
    +}
    +
     func (it *entryIteratorDS) Entry() *types.Entry {
     	return it.entries[it.next-1]
     }
    
  • pkg/server/endpoints/endpoints_test.go+2 2 modified
    @@ -47,8 +47,8 @@ import (
     
     var (
     	testTD       = spiffeid.RequireTrustDomainFromString("domain.test")
    -	serverID     = testTD.NewID("/server")
    -	agentID      = testTD.NewID("/agent")
    +	serverID     = testTD.NewID("/spire/server")
    +	agentID      = testTD.NewID("/spire/agent/foo")
     	adminID      = testTD.NewID("/admin")
     	downstreamID = testTD.NewID("/downstream")
     	rateLimit    = RateLimitConfig{Attestation: true}
    
  • pkg/server/endpoints/middleware_test.go+4 4 modified
    @@ -134,7 +134,7 @@ func TestAgentAuthorizer(t *testing.T) {
     				CertSerialNumber: agentSVID.SerialNumber.String(),
     			},
     			expectedCode:   codes.PermissionDenied,
    -			expectedMsg:    `agent "spiffe://domain.test/agent" SVID is expired`,
    +			expectedMsg:    `agent "spiffe://domain.test/spire/agent/foo" SVID is expired`,
     			expectedReason: types.PermissionDeniedDetails_AGENT_EXPIRED,
     			expectedLogs: []spiretest.LogEntry{
     				{
    @@ -149,7 +149,7 @@ func TestAgentAuthorizer(t *testing.T) {
     		{
     			name:           "no attested node",
     			expectedCode:   codes.PermissionDenied,
    -			expectedMsg:    `agent "spiffe://domain.test/agent" is not attested`,
    +			expectedMsg:    `agent "spiffe://domain.test/spire/agent/foo" is not attested`,
     			expectedReason: types.PermissionDeniedDetails_AGENT_NOT_ATTESTED,
     			expectedLogs: []spiretest.LogEntry{
     				{
    @@ -167,7 +167,7 @@ func TestAgentAuthorizer(t *testing.T) {
     				SpiffeId: agentID.String(),
     			},
     			expectedCode:   codes.PermissionDenied,
    -			expectedMsg:    `agent "spiffe://domain.test/agent" is banned`,
    +			expectedMsg:    `agent "spiffe://domain.test/spire/agent/foo" is banned`,
     			expectedReason: types.PermissionDeniedDetails_AGENT_BANNED,
     			expectedLogs: []spiretest.LogEntry{
     				{
    @@ -186,7 +186,7 @@ func TestAgentAuthorizer(t *testing.T) {
     				CertSerialNumber: "NEW",
     			},
     			expectedCode:   codes.PermissionDenied,
    -			expectedMsg:    fmt.Sprintf(`agent "spiffe://domain.test/agent" expected to have serial number "NEW"; has %q`, agentSVID.SerialNumber.String()),
    +			expectedMsg:    fmt.Sprintf(`agent "spiffe://domain.test/spire/agent/foo" expected to have serial number "NEW"; has %q`, agentSVID.SerialNumber.String()),
     			expectedReason: types.PermissionDeniedDetails_AGENT_NOT_ACTIVE,
     			expectedLogs: []spiretest.LogEntry{
     				{
    
  • pkg/server/endpoints/node/handler.go+23 5 modified
    @@ -173,6 +173,11 @@ func (h *Handler) Attest(stream node.Node_AttestServer) (err error) {
     	agentID := attestResponse.AgentId
     	log = log.WithField(telemetry.SPIFFEID, agentID)
     
    +	if err := idutil.CheckAgentIDStringNormalization(agentID); err != nil {
    +		log.WithError(err).Error("Agent ID is malformed")
    +		return status.Errorf(codes.Internal, "agent ID is malformed: %v", err)
    +	}
    +
     	isBanned, err := h.isBanned(ctx, agentID)
     	switch {
     	case err != nil:
    @@ -359,6 +364,11 @@ func (h *Handler) FetchX509CASVID(ctx context.Context, req *node.FetchX509CASVID
     		return nil, status.Error(codes.InvalidArgument, err.Error())
     	}
     
    +	if err := idutil.CheckIDStringNormalization(csr.SpiffeID); err != nil {
    +		log.WithError(err).Error("CSR SPIFFE ID is malformed")
    +		return nil, status.Errorf(codes.InvalidArgument, "CSR SPIFFE ID is malformed: %v", err)
    +	}
    +
     	signLog.Debug("Signing downstream CA SVID")
     	svid, err := h.buildCASVID(ctx, ca.X509CASVIDParams{
     		SpiffeID:  h.c.TrustDomain.String(),
    @@ -1170,14 +1180,22 @@ func tryGetSpiffeIDFromCert(cert *x509.Certificate) string {
     }
     
     func getSpiffeIDFromCert(cert *x509.Certificate) (string, error) {
    -	if len(cert.URIs) == 0 {
    -		return "", errors.New("no URI SANs in certificate")
    -	}
    -	spiffeID, err := idutil.NormalizeSpiffeIDURL(cert.URIs[0], idutil.AllowAny())
    +	uri, err := getURISANFromCert(cert)
     	if err != nil {
     		return "", err
     	}
    -	return spiffeID.String(), nil
    +	return uri.String(), nil
    +}
    +
    +func getURISANFromCert(cert *x509.Certificate) (*url.URL, error) {
    +	if len(cert.URIs) == 0 {
    +		return nil, errors.New("no URI SANs in certificate")
    +	}
    +	uri := cert.URIs[0]
    +	if err := idutil.CheckIDURLNormalization(uri); err != nil {
    +		return nil, fmt.Errorf("URI SAN %q is malformed: %v", uri, err)
    +	}
    +	return idutil.NormalizeSpiffeIDURL(uri, idutil.AllowAny())
     }
     
     func makeX509SVID(svid []*x509.Certificate) *node.X509SVID {
    
  • pkg/server/endpoints/registration/handler.go+19 3 modified
    @@ -749,12 +749,16 @@ func (h *Handler) normalizeSPIFFEIDForMinting(spiffeID string) (string, error) {
     		return "", status.Error(codes.InvalidArgument, "request missing SPIFFE ID")
     	}
     
    -	spiffeID, err := idutil.NormalizeSpiffeID(spiffeID, idutil.AllowTrustDomainWorkload(h.TrustDomain.Host))
    +	normalized, err := idutil.NormalizeSpiffeID(spiffeID, idutil.AllowTrustDomainWorkload(h.TrustDomain.Host))
     	if err != nil {
    -		return "", status.Errorf(codes.InvalidArgument, err.Error())
    +		return "", status.Error(codes.InvalidArgument, err.Error())
     	}
     
    -	return spiffeID, nil
    +	if err := idutil.CheckIDStringNormalization(spiffeID); err != nil {
    +		return "", status.Errorf(codes.InvalidArgument, "spiffe ID is malformed: %v", err)
    +	}
    +
    +	return normalized, nil
     }
     
     func (h *Handler) isEntryUnique(ctx context.Context, ds datastore.DataStore, entry *common.RegistrationEntry) (*common.RegistrationEntry, bool, error) {
    @@ -816,6 +820,7 @@ func (h *Handler) createRegistrationEntry(ctx context.Context, requestedEntry *c
     	return createResponse.Entry, false, nil
     }
     func (h *Handler) prepareRegistrationEntry(entry *common.RegistrationEntry, forUpdate bool) (*common.RegistrationEntry, error) {
    +	original := entry
     	entry = cloneRegistrationEntry(entry)
     	if forUpdate && entry.EntryId == "" {
     		return nil, errors.New("missing registration entry id")
    @@ -840,6 +845,14 @@ func (h *Handler) prepareRegistrationEntry(entry *common.RegistrationEntry, forU
     		return nil, err
     	}
     
    +	if err := idutil.CheckIDStringNormalization(original.ParentId); err != nil {
    +		return nil, fmt.Errorf("parent ID is malformed: %v", err)
    +	}
    +
    +	if err := idutil.CheckIDStringNormalization(original.SpiffeId); err != nil {
    +		return nil, fmt.Errorf("spiffe ID is malformed: %v", err)
    +	}
    +
     	// Validate Selectors
     	for _, s := range entry.Selectors {
     		if err := selector.Validate(s); err != nil {
    @@ -887,6 +900,9 @@ func getSpiffeIDFromCert(cert *x509.Certificate) (string, error) {
     	if len(cert.URIs) == 0 {
     		return "", errors.New("no SPIFFE ID in certificate")
     	}
    +	if err := idutil.CheckIDURLNormalization(cert.URIs[0]); err != nil {
    +		return "", fmt.Errorf("SPIFFE ID is malformed: %v", err)
    +	}
     	spiffeID, err := idutil.NormalizeSpiffeIDURL(cert.URIs[0], idutil.AllowAny())
     	if err != nil {
     		return "", err
    
  • pkg/server/endpoints/registration/handler_test.go+1 1 modified
    @@ -1398,7 +1398,7 @@ func (s *HandlerSuite) TestAuthorizeCall() {
     		},
     		{
     			Peer: makeTLSPeer("whatever://example.org"),
    -			Err:  "not a valid SPIFFE ID",
    +			Err:  "SPIFFE ID is malformed: scheme must be 'spiffe'",
     		},
     		{
     			Peer: makeTLSPeer("spiffe://example.org/not-admin"),
    
  • pkg/server/scanentries.go+66 0 added
    @@ -0,0 +1,66 @@
    +package server
    +
    +import (
    +	"context"
    +
    +	"github.com/sirupsen/logrus"
    +	"github.com/spiffe/spire/pkg/common/idutil"
    +	"github.com/spiffe/spire/pkg/common/telemetry"
    +	serverTelemetry "github.com/spiffe/spire/pkg/common/telemetry/server"
    +	"github.com/spiffe/spire/pkg/server/plugin/datastore"
    +)
    +
    +var (
    +	// entryScanPageSize defaults to 200 but can be mutated by unit tests to
    +	// easier test page handling
    +	entryScanPageSize int32 = 200
    +)
    +
    +// scanForBadEntries scans the entry list for entries that we will ignore
    +// because they contain problematic spiffe IDs. It will never return an
    +// error since it is advisory only.
    +func scanForBadEntries(log logrus.FieldLogger, metrics telemetry.Metrics, ds datastore.DataStore) func(ctx context.Context) error {
    +	return func(ctx context.Context) error {
    +		pagination := &datastore.Pagination{PageSize: entryScanPageSize}
    +		ignored := 0
    +		for {
    +			resp, err := ds.ListRegistrationEntries(ctx, &datastore.ListRegistrationEntriesRequest{
    +				Pagination: pagination,
    +			})
    +			if err != nil {
    +				log.WithError(err).Warn("Failed to scan for bad entries")
    +				return nil
    +			}
    +
    +			for _, entry := range resp.Entries {
    +				if err := idutil.CheckIDStringNormalization(entry.ParentId); err != nil {
    +					log.WithFields(logrus.Fields{
    +						telemetry.ParentID:       entry.ParentId,
    +						telemetry.SPIFFEID:       entry.SpiffeId,
    +						telemetry.RegistrationID: entry.EntryId,
    +						logrus.ErrorKey:          err,
    +					}).Error("Ignoring entry with invalid parentID; this entry will be automatically deleted by a future release")
    +					ignored++
    +					continue
    +				}
    +				if err := idutil.CheckIDStringNormalization(entry.SpiffeId); err != nil {
    +					log.WithFields(logrus.Fields{
    +						telemetry.ParentID:       entry.ParentId,
    +						telemetry.SPIFFEID:       entry.SpiffeId,
    +						telemetry.RegistrationID: entry.EntryId,
    +						logrus.ErrorKey:          err,
    +					}).Error("Ignoring entry with invalid spiffeID; this entry will be automatically deleted by a future release")
    +					ignored++
    +					continue
    +				}
    +			}
    +
    +			switch {
    +			case resp.Pagination == nil, resp.Pagination.Token == "":
    +				serverTelemetry.SetEntryIgnoredGauge(metrics, ignored)
    +				return nil
    +			}
    +			pagination.Token = resp.Pagination.Token
    +		}
    +	}
    +}
    
  • pkg/server/scanentries_test.go+134 0 added
    @@ -0,0 +1,134 @@
    +package server
    +
    +import (
    +	"context"
    +	"testing"
    +
    +	"github.com/sirupsen/logrus"
    +	"github.com/sirupsen/logrus/hooks/test"
    +	"github.com/spiffe/spire/pkg/server/plugin/datastore"
    +	"github.com/spiffe/spire/proto/spire/common"
    +	"github.com/spiffe/spire/test/fakes/fakedatastore"
    +	"github.com/spiffe/spire/test/fakes/fakemetrics"
    +	"github.com/spiffe/spire/test/spiretest"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func init() {
    +	// Use a page size of two for unit tests
    +	entryScanPageSize = 2
    +}
    +
    +func TestScanForBadEntries(t *testing.T) {
    +	good1 := &common.RegistrationEntry{
    +		ParentId:  "spiffe://example.org/node",
    +		SpiffeId:  "spiffe://example.org/workload",
    +		Selectors: []*common.Selector{{Type: "one", Value: "1"}},
    +	}
    +	good2 := &common.RegistrationEntry{
    +		ParentId:  "spiffe://example.org/node",
    +		SpiffeId:  "spiffe://example.org/workload",
    +		Selectors: []*common.Selector{{Type: "two", Value: "2"}},
    +	}
    +	good3 := &common.RegistrationEntry{
    +		ParentId:  "spiffe://example.org/node",
    +		SpiffeId:  "spiffe://example.org/workload",
    +		Selectors: []*common.Selector{{Type: "three", Value: "3"}},
    +	}
    +	badParentID := &common.RegistrationEntry{
    +		ParentId:  "spiffe://example.org//node",
    +		SpiffeId:  "spiffe://example.org/workload",
    +		Selectors: []*common.Selector{{Type: "bad", Value: "parent-id"}},
    +	}
    +	badSpiffeID := &common.RegistrationEntry{
    +		ParentId:  "spiffe://example.org/node",
    +		SpiffeId:  "spiffe://example.org//workload",
    +		Selectors: []*common.Selector{{Type: "bad", Value: "spiffe-id"}},
    +	}
    +
    +	for _, tt := range []struct {
    +		name          string
    +		entries       []*common.RegistrationEntry
    +		expectLogs    []spiretest.LogEntry
    +		expectIgnored int
    +	}{
    +		{
    +			name:          "no entries",
    +			expectIgnored: 0,
    +		},
    +		{
    +			name:          "no bad entries",
    +			entries:       []*common.RegistrationEntry{good1, good2, good3},
    +			expectIgnored: 0,
    +		},
    +		{
    +			name:    "bad spiffe id",
    +			entries: []*common.RegistrationEntry{good1, good2, badSpiffeID, good3},
    +			expectLogs: []spiretest.LogEntry{
    +				{
    +					Level:   logrus.ErrorLevel,
    +					Message: "Ignoring entry with invalid spiffeID; this entry will be automatically deleted by a future release",
    +					Data: logrus.Fields{
    +						"entry_id":  "SOME-ID",
    +						"error":     "path cannot contain empty, '.', or '..' segments",
    +						"parent_id": "spiffe://example.org/node",
    +						"spiffe_id": "spiffe://example.org//workload",
    +					},
    +				},
    +			},
    +			expectIgnored: 1,
    +		},
    +		{
    +			name:    "bad parent id",
    +			entries: []*common.RegistrationEntry{good1, good2, badParentID, good3},
    +			expectLogs: []spiretest.LogEntry{
    +				{
    +					Level:   logrus.ErrorLevel,
    +					Message: "Ignoring entry with invalid parentID; this entry will be automatically deleted by a future release",
    +					Data: logrus.Fields{
    +						"entry_id":  "SOME-ID",
    +						"error":     "path cannot contain empty, '.', or '..' segments",
    +						"parent_id": "spiffe://example.org//node",
    +						"spiffe_id": "spiffe://example.org/workload",
    +					},
    +				},
    +			},
    +			expectIgnored: 1,
    +		},
    +	} {
    +		tt := tt
    +		t.Run(tt.name, func(t *testing.T) {
    +			log, logHook := test.NewNullLogger()
    +			metrics := fakemetrics.New()
    +			ds := fakedatastore.New(t)
    +
    +			for _, entry := range tt.entries {
    +				_, err := ds.CreateRegistrationEntry(context.Background(), &datastore.CreateRegistrationEntryRequest{
    +					Entry: entry,
    +				})
    +				require.NoError(t, err)
    +			}
    +
    +			require.NoError(t, scanForBadEntries(log, metrics, ds)(context.Background()))
    +
    +			// Assert the logs are as expected. The Entry IDs need to be
    +			// patched up in the log entries since they are non-deterministic.
    +			logEntries := logHook.AllEntries()
    +			for _, logEntry := range logEntries {
    +				if _, ok := logEntry.Data["entry_id"]; ok {
    +					logEntry.Data["entry_id"] = "SOME-ID"
    +				}
    +			}
    +			spiretest.AssertLogs(t, logEntries, tt.expectLogs)
    +
    +			assert.Equal(t, []fakemetrics.MetricItem{
    +				{
    +					Type: fakemetrics.SetGaugeType,
    +					Key:  []string{"entry", "ignored"},
    +					Val:  float32(tt.expectIgnored),
    +				},
    +			}, metrics.AllMetrics())
    +		})
    +	}
    +}
    
  • pkg/server/server.go+1 0 modified
    @@ -171,6 +171,7 @@ func (s *Server) run(ctx context.Context) (err error) {
     		bundleManager.Run,
     		registrationManager.Run,
     		healthChecks.ListenAndServe,
    +		scanForBadEntries(s.config.Log, metrics, cat.GetDataStore()),
     	)
     	if err == context.Canceled {
     		err = nil
    
  • pkg/server/util/regentryutil/fetch.go+20 2 modified
    @@ -5,6 +5,7 @@ import (
     	"errors"
     	"time"
     
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/pkg/common/util"
     	"github.com/spiffe/spire/pkg/server/cache/entrycache"
     	"github.com/spiffe/spire/pkg/server/plugin/datastore"
    @@ -108,7 +109,7 @@ func (f *registrationEntryFetcher) childEntries(ctx context.Context, clientID st
     		return nil, err
     	}
     
    -	return resp.Entries, nil
    +	return f.filterEntries(resp.Entries), nil
     }
     
     // mappedEntries returns all registration entries for which the given ID has
    @@ -145,7 +146,24 @@ func (f *registrationEntryFetcher) mappedEntries(ctx context.Context, clientID s
     		return nil, err
     	}
     
    -	return listResp.Entries, nil
    +	return f.filterEntries(listResp.Entries), nil
    +}
    +
    +func (f *registrationEntryFetcher) filterEntries(in []*common.RegistrationEntry) []*common.RegistrationEntry {
    +	out := make([]*common.RegistrationEntry, 0, len(in))
    +	for _, entry := range in {
    +		// Filter out entries with invalid SPIFFE IDs. Operators are notified
    +		// that they are ignored on server startup (see
    +		// pkg/server/scanentries.go)
    +		if err := idutil.CheckIDStringNormalization(entry.SpiffeId); err != nil {
    +			continue
    +		}
    +		if err := idutil.CheckIDStringNormalization(entry.ParentId); err != nil {
    +			continue
    +		}
    +		out = append(out, entry)
    +	}
    +	return out
     }
     
     type nullCache struct {
    
  • pkg/server/util/regentryutil/fetch_test.go+2 0 modified
    @@ -48,6 +48,7 @@ func TestFetchRegistrationEntries(t *testing.T) {
     	threeID := "spiffe://example.org/3"
     	fourID := "spiffe://example.org/4"
     	fiveID := "spiffe://example.org/5"
    +	someParentID := "spiffe://example.org/parent"
     
     	a1 := &common.Selector{Type: "a", Value: "1"}
     	b2 := &common.Selector{Type: "b", Value: "2"}
    @@ -80,6 +81,7 @@ func TestFetchRegistrationEntries(t *testing.T) {
     	})
     
     	fourEntry := createRegistrationEntry(&common.RegistrationEntry{
    +		ParentId:  someParentID,
     		SpiffeId:  fourID,
     		Selectors: []*common.Selector{a1, b2},
     	})
    
  • support/k8s/k8s-workload-registrar/config_reconcile.go+3 3 modified
    @@ -3,7 +3,6 @@ package main
     import (
     	"context"
     	"fmt"
    -	"path"
     
     	corev1 "k8s.io/api/core/v1"
     	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    @@ -17,6 +16,7 @@ import (
     	ctrl "sigs.k8s.io/controller-runtime"
     	"sigs.k8s.io/controller-runtime/pkg/log/zap"
     
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	spiretypes "github.com/spiffe/spire/proto/spire/types"
     	"github.com/spiffe/spire/support/k8s/k8s-workload-registrar/mode-reconcile/controllers"
     	"github.com/zeebo/errs"
    @@ -161,13 +161,13 @@ func (slw SpiffeLogWrapper) Errorf(format string, args ...interface{}) {
     func ServerID(trustDomain string) *spiretypes.SPIFFEID {
     	return &spiretypes.SPIFFEID{
     		TrustDomain: trustDomain,
    -		Path:        path.Join("/", "spire", "server"),
    +		Path:        idutil.JoinPathSegments("spire", "server"),
     	}
     }
     
     func nodeID(trustDomain string, controllerName string, cluster string) *spiretypes.SPIFFEID {
     	return &spiretypes.SPIFFEID{
     		TrustDomain: trustDomain,
    -		Path:        path.Join("/", controllerName, cluster, "node"),
    +		Path:        idutil.JoinPathSegments(controllerName, cluster, "node"),
     	}
     }
    
  • support/k8s/k8s-workload-registrar/controller.go+1 2 modified
    @@ -6,7 +6,6 @@ import (
     	"encoding/json"
     	"errors"
     	"fmt"
    -	"path"
     
     	"github.com/sirupsen/logrus"
     	"github.com/spiffe/spire/pkg/common/idutil"
    @@ -207,7 +206,7 @@ func (c *Controller) nodeID() *types.SPIFFEID {
     func (c *Controller) makeID(pathFmt string, pathArgs ...interface{}) *types.SPIFFEID {
     	return &types.SPIFFEID{
     		TrustDomain: c.c.TrustDomain,
    -		Path:        path.Clean("/" + fmt.Sprintf(pathFmt, pathArgs...)),
    +		Path:        idutil.FormatPath(pathFmt, pathArgs...),
     	}
     }
     
    
  • support/k8s/k8s-workload-registrar/controller_test.go+1 2 modified
    @@ -3,7 +3,6 @@ package main
     import (
     	"context"
     	"fmt"
    -	"path"
     	"sort"
     	"sync"
     	"testing"
    @@ -581,5 +580,5 @@ func stringFromID(id *types.SPIFFEID) string {
     	if id == nil {
     		return ""
     	}
    -	return fmt.Sprintf("spiffe://%s%s", id.TrustDomain, path.Clean("/"+id.Path))
    +	return fmt.Sprintf("spiffe://%s%s", id.TrustDomain, id.Path)
     }
    
  • support/k8s/k8s-workload-registrar/mode-crd/api/spiffeid/v1beta1/spiffeid_webhook.go+2 2 modified
    @@ -21,7 +21,7 @@ import (
     	"strings"
     
     	"github.com/sirupsen/logrus"
    -	"github.com/spiffe/go-spiffe/v2/spiffeid"
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/pkg/common/x509util"
     	"github.com/spiffe/spire/proto/spire/api/server/entry/v1"
     	"github.com/spiffe/spire/proto/spire/types"
    @@ -74,7 +74,7 @@ func (s *SpiffeID) ValidateCreate() error {
     	}
     
     	for _, entry := range resp.Entries {
    -		entrySPIFFEID, err := spiffeid.New(entry.SpiffeId.TrustDomain, entry.SpiffeId.Path)
    +		entrySPIFFEID, err := idutil.IDFromProto(entry.SpiffeId)
     		if err != nil {
     			return fmt.Errorf("entry SPIFFE ID is malformed: %v", err)
     		}
    
  • support/k8s/k8s-workload-registrar/mode-crd/controllers/spiffeid_controller_test.go+1 2 modified
    @@ -17,7 +17,6 @@ package controllers
     
     import (
     	"fmt"
    -	"path"
     	"testing"
     
     	entryv1 "github.com/spiffe/spire/proto/spire/api/server/entry/v1"
    @@ -119,5 +118,5 @@ func (s *SpiffeIDControllerTestSuite) TestCreateSpiffeID() {
     }
     
     func stringFromID(id *spireTypes.SPIFFEID) string {
    -	return fmt.Sprintf("spiffe://%s%s", id.TrustDomain, path.Clean("/"+id.Path))
    +	return fmt.Sprintf("spiffe://%s%s", id.TrustDomain, id.Path)
     }
    
  • support/k8s/k8s-workload-registrar/mode-crd/controllers/utils.go+2 3 modified
    @@ -18,10 +18,9 @@ package controllers
     import (
     	"context"
     	"errors"
    -	"fmt"
     	"net/url"
    -	"path"
     
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	entryv1 "github.com/spiffe/spire/proto/spire/api/server/entry/v1"
     	spiffeidv1beta1 "github.com/spiffe/spire/support/k8s/k8s-workload-registrar/mode-crd/api/spiffeid/v1beta1"
     	"google.golang.org/grpc/codes"
    @@ -100,7 +99,7 @@ func makeID(trustDomain, pathFmt string, pathArgs ...interface{}) string {
     	id := url.URL{
     		Scheme: "spiffe",
     		Host:   trustDomain,
    -		Path:   path.Clean(fmt.Sprintf(pathFmt, pathArgs...)),
    +		Path:   idutil.FormatPath(pathFmt, pathArgs...),
     	}
     	return id.String()
     }
    
  • support/k8s/k8s-workload-registrar/mode-reconcile/controllers/node_controller.go+2 2 modified
    @@ -18,9 +18,9 @@ package controllers
     import (
     	"context"
     	"fmt"
    -	"path"
     	"strings"
     
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/proto/spire/api/server/entry/v1"
     	spiretypes "github.com/spiffe/spire/proto/spire/types"
     	"google.golang.org/protobuf/proto"
    @@ -58,7 +58,7 @@ func (r *NodeReconciler) shouldProcess(_ ctrl.Request) bool {
     func (r *NodeReconciler) makeSpiffeID(obj ObjectWithMetadata) *spiretypes.SPIFFEID {
     	return &spiretypes.SPIFFEID{
     		TrustDomain: r.RootID.TrustDomain,
    -		Path:        path.Join(r.RootID.Path, obj.GetName()),
    +		Path:        r.RootID.Path + idutil.JoinPathSegments(obj.GetName()),
     	}
     }
     
    
  • support/k8s/k8s-workload-registrar/mode-reconcile/controllers/node_controller_test.go+2 2 modified
    @@ -2,9 +2,9 @@ package controllers
     
     import (
     	"context"
    -	"path"
     	"testing"
     
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/proto/spire/api/server/entry/v1"
     	"github.com/spiffe/spire/test/fakes/fakeentryclient"
     
    @@ -64,7 +64,7 @@ func (s *NodeControllerTestSuite) TearDownTest() {
     func (s *NodeControllerTestSuite) makeNodeID(node string) *spiretypes.SPIFFEID {
     	return &spiretypes.SPIFFEID{
     		TrustDomain: nodeControllerTestTrustDomain,
    -		Path:        path.Join("foo", "node", node),
    +		Path:        idutil.JoinPathSegments("foo", "node", node),
     	}
     }
     
    
  • support/k8s/k8s-workload-registrar/mode-reconcile/controllers/pod_controller.go+7 7 modified
    @@ -18,10 +18,10 @@ package controllers
     import (
     	"context"
     	"fmt"
    -	"path"
     	"sort"
     	"strings"
     
    +	"github.com/spiffe/spire/pkg/common/idutil"
     	"github.com/spiffe/spire/proto/spire/api/server/entry/v1"
     	spiretypes "github.com/spiffe/spire/proto/spire/types"
     
    @@ -229,23 +229,23 @@ func (r *PodReconciler) makeSpiffeIDForPod(pod *corev1.Pod) *spiretypes.SPIFFEID
     	var spiffeID *spiretypes.SPIFFEID
     	switch r.Mode {
     	case PodReconcilerModeServiceAccount:
    -		spiffeID = r.makeID(path.Join("/ns", pod.Namespace, "sa", pod.Spec.ServiceAccountName))
    +		spiffeID = r.makeID("ns", pod.Namespace, "sa", pod.Spec.ServiceAccountName)
     	case PodReconcilerModeLabel:
     		if val, ok := pod.GetLabels()[r.Value]; ok {
    -			spiffeID = r.makeID(path.Join("/", val))
    +			spiffeID = r.makeID(val)
     		}
     	case PodReconcilerModeAnnotation:
     		if val, ok := pod.GetAnnotations()[r.Value]; ok {
    -			spiffeID = r.makeID(path.Join("/", val))
    +			spiffeID = r.makeID(val)
     		}
     	}
     	return spiffeID
     }
     
    -func (r *PodReconciler) makeID(path string) *spiretypes.SPIFFEID {
    +func (r *PodReconciler) makeID(segments ...string) *spiretypes.SPIFFEID {
     	return &spiretypes.SPIFFEID{
     		TrustDomain: r.TrustDomain,
    -		Path:        path,
    +		Path:        idutil.JoinPathSegments(segments...),
     	}
     }
     
    @@ -256,7 +256,7 @@ func (r *PodReconciler) makeParentIDForPod(pod *corev1.Pod) *spiretypes.SPIFFEID
     	}
     	return &spiretypes.SPIFFEID{
     		TrustDomain: r.RootID.TrustDomain,
    -		Path:        path.Join(r.RootID.Path, nodeName),
    +		Path:        r.RootID.Path + idutil.JoinPathSegments(nodeName),
     	}
     }
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.