CVE-2025-24976 (Go)

Distribution is a toolkit to pack, ship, store, and deliver container content. Systems running registry versions 3.0.0-beta.1 through 3.0.0-rc.2 with token authentication enabled may be vulnerable to an issue in which token authentication allows an attacker to inject an untrusted signing key in a JSON web token (JWT). The issue lies in how the JSON web key (JWK) verification is performed. When a JWT contains a JWK header without a certificate chain, the code only checks if the KeyID (`kid`) matches one of the trusted keys, but doesn't verify that the actual key material matches. A fix for the issue is available at commit 5ea9aa028db65ca5665f6af2c20ecf9dc34e5fcd and expected to be a part of version 3.0.0-rc.3. There is no way to work around this issue without patching if the system requires token authentication.

Weakness:
CWE-639: name: Authorization Bypass Through User-Controlled Key
description: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.

Repository: https://github.com/distribution/distribution

Vulnerable code:
file_path: registry/auth/token/token.go
snippet: func verifyJWK(header jose.Header, verifyOpts VerifyOptions) (signingKey crypto.PublicKey, err error) {
	jwk := header.JSONWebKey
	signingKey = jwk.Key

	// Check to see if the key includes a certificate chain.
	if len(jwk.Certificates) == 0 {
		// The JWK should be one of the trusted root keys.
		if _, trusted := verifyOpts.TrustedKeys[jwk.KeyID]; !trusted {
			return nil, errors.New("untrusted JWK with no certificate chain")
		}
		// The JWK is one of the trusted keys.
		return
	}

	opts := x509.VerifyOptions{
		Roots:     verifyOpts.Roots,
		KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
	}

	leaf := jwk.Certificates[0]
	if opts.Intermediates == nil {
		opts.Intermediates = x509.NewCertPool()
		for _, intermediate := range jwk.Certificates[1:] {
			opts.Intermediates.AddCert(intermediate)
		}
	}

	// TODO: this call returns certificate chains which we ignore for now, but
	// we should check them for revocations if we have the ability later.
	chains, err := leaf.Verify(opts)
	if err != nil {
		return nil, err
	}
	signingKey = getCertPubKey(chains)

	return
}

Fixed code:
file_path: registry/auth/token/token.go
snippet: func verifyJWK(header jose.Header, verifyOpts VerifyOptions) (crypto.PublicKey, error) {
	jwk := header.JSONWebKey

	// Check to see if the key includes a certificate chain.
	if len(jwk.Certificates) == 0 {
		// The JWK should be one of the trusted root keys.
		key, trusted := verifyOpts.TrustedKeys[jwk.KeyID]
		if !trusted {
			return nil, errors.New("untrusted JWK with no certificate chain")
		}
		// The JWK is one of the trusted keys.
		return key, nil
	}

	opts := x509.VerifyOptions{
		Roots:     verifyOpts.Roots,
		KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
	}

	leaf := jwk.Certificates[0]
	if opts.Intermediates == nil {
		opts.Intermediates = x509.NewCertPool()
		for _, intermediate := range jwk.Certificates[1:] {
			opts.Intermediates.AddCert(intermediate)
		}
	}

	// TODO: this call returns certificate chains which we ignore for now, but
	// we should check them for revocations if we have the ability later.
	chains, err := leaf.Verify(opts)
	if err != nil {
		return nil, err
	}

	return getCertPubKey(chains), nil
}