CVE-2025-24882 (Go)

regclient does not reliably enforce the integrity of digest-pinned manifests when constructing manifest objects from registry responses. A malicious registry can control the returned manifest body and response metadata such as Docker-Content-Digest, and can provide digest metadata that conflicts with a digest pinned in the requested reference or supplied descriptor. If regclient allows registry-supplied metadata to overwrite, hide, or satisfy only one expected digest, or skips verification for a supported manifest format such as signed Docker schema1, it may accept a manifest whose actual or format-defined canonical content does not match the digest the caller intended to trust. This breaks the security guarantee of digest-pinned pulls and can allow substitution of a different image manifest or image content. The fix should treat all caller-supplied or accepted expected manifest digests from the reference, descriptor, and registry response metadata as integrity constraints; recompute the digest over the appropriate bytes or canonical representation for the manifest type using the expected digest's algorithm; reject mismatches or conflicting digest sources; and ensure untrusted registry metadata cannot bypass a pinned reference digest.

Weakness:
CWE-345: name: Insufficient Verification of Data Authenticity
description: The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.

Repository: https://github.com/regclient/regclient

Vulnerable code:
file_path: types/manifest/manifest.go
snippet: func New(opts ...Opts) (Manifest, error) {
	mc := manifestConfig{}
	for _, opt := range opts {
		opt(&mc)
	}
	c := common{
		r:         mc.r,
		desc:      mc.desc,
		rawBody:   mc.raw,
		rawHeader: mc.header,
	}
	// extract fields from header where available
	if mc.header != nil {
		if c.desc.MediaType == "" {
			c.desc.MediaType = mediatype.Base(mc.header.Get("Content-Type"))
		}
		if c.desc.Size == 0 {
			cl, _ := strconv.Atoi(mc.header.Get("Content-Length"))
			c.desc.Size = int64(cl)
		}
		if c.desc.Digest == "" {
			c.desc.Digest, _ = digest.Parse(mc.header.Get("Docker-Content-Digest"))
		}
		c.setRateLimit(mc.header)
	}
	if mc.orig != nil {
		return fromOrig(c, mc.orig)
	}
	return fromCommon(c)
}

file_path: types/manifest/manifest.go
snippet: func fromOrig(c common, orig interface{}) (Manifest, error) {
	var mt string
	var m Manifest
	origDigest := c.desc.Digest

	mj, err := json.Marshal(orig)
	if err != nil {
		return nil, err
	}
	c.manifSet = true
	if len(c.rawBody) == 0 {
		c.rawBody = mj
	}
	if _, ok := orig.(schema1.SignedManifest); !ok {
		c.desc.Digest = c.desc.DigestAlgo().FromBytes(mj)
	}
	if c.desc.Size == 0 {
		c.desc.Size = int64(len(mj))
	}
	// create manifest based on type
	switch mOrig := orig.(type) {
	case schema1.Manifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker1Manifest
		m = &docker1Manifest{
			common:   c,
			Manifest: mOrig,
		}
	case schema1.SignedManifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker1ManifestSigned
		// recompute digest on the canonical data
		c.desc.Digest = c.desc.DigestAlgo().FromBytes(mOrig.Canonical)
		m = &docker1SignedManifest{
			common:         c,
			SignedManifest: mOrig,
		}
	case schema2.Manifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker2Manifest
		m = &docker2Manifest{
			common:   c,
			Manifest: mOrig,
		}
	case schema2.ManifestList:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker2ManifestList
		m = &docker2ManifestList{
			common:       c,
			ManifestList: mOrig,
		}
	case v1.Manifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.OCI1Manifest
		m = &oci1Manifest{
			common:   c,
			Manifest: mOrig,
		}
	case v1.Index:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.OCI1ManifestList
		m = &oci1Index{
			common: c,
			Index:  orig.(v1.Index),
		}
	case v1.ArtifactManifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.OCI1Artifact
		m = &oci1Artifact{
			common:           c,
			ArtifactManifest: mOrig,
		}
	default:
		return nil, fmt.Errorf("unsupported type to convert to a manifest: %T", orig)
	}
	// verify media type
	err = verifyMT(c.desc.MediaType, mt)
	if err != nil {
		return nil, err
	}
	// verify digest didn't change
	if origDigest != "" && origDigest != c.desc.Digest {
		return nil, fmt.Errorf("manifest digest mismatch, expected %s, computed %s", origDigest, c.desc.Digest)
	}
	return m, nil
}

file_path: types/manifest/manifest.go
snippet: func fromCommon(c common) (Manifest, error) {
	var err error
	var m Manifest
	var mt string
	origDigest := c.desc.Digest
	// extract common data from from rawBody
	if len(c.rawBody) > 0 {
		c.manifSet = true
		// extract media type from body, either explicitly or with duck typing
		if c.desc.MediaType == "" {
			mt := struct {
				MediaType     string                  `json:"mediaType,omitempty"`
				SchemaVersion int                     `json:"schemaVersion,omitempty"`
				Signatures    []interface{}           `json:"signatures,omitempty"`
				Manifests     []descriptor.Descriptor `json:"manifests,omitempty"`
				Layers        []descriptor.Descriptor `json:"layers,omitempty"`
			}{}
			err = json.Unmarshal(c.rawBody, &mt)
			if mt.MediaType != "" {
				c.desc.MediaType = mt.MediaType
			} else if mt.SchemaVersion == 1 && len(mt.Signatures) > 0 {
				c.desc.MediaType = mediatype.Docker1ManifestSigned
			} else if mt.SchemaVersion == 1 {
				c.desc.MediaType = mediatype.Docker1Manifest
			} else if len(mt.Manifests) > 0 {
				if strings.HasPrefix(mt.Manifests[0].MediaType, "application/vnd.docker.") {
					c.desc.MediaType = mediatype.Docker2ManifestList
				} else {
					c.desc.MediaType = mediatype.OCI1ManifestList
				}
			} else if len(mt.Layers) > 0 {
				if strings.HasPrefix(mt.Layers[0].MediaType, "application/vnd.docker.") {
					c.desc.MediaType = mediatype.Docker2Manifest
				} else {
					c.desc.MediaType = mediatype.OCI1Manifest
				}
			}
		}
		// compute digest
		if c.desc.MediaType != mediatype.Docker1ManifestSigned {
			d := c.desc.DigestAlgo().FromBytes(c.rawBody)
			c.desc.Digest = d
			c.desc.Size = int64(len(c.rawBody))
		}
	}
	switch c.desc.MediaType {
	case mediatype.Docker1Manifest:
		var mOrig schema1.Manifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &docker1Manifest{common: c, Manifest: mOrig}
	case mediatype.Docker1ManifestSigned:
		var mOrig schema1.SignedManifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
			d := c.desc.DigestAlgo().FromBytes(mOrig.Canonical)
			c.desc.Digest = d
			c.desc.Size = int64(len(mOrig.Canonical))
		}
		m = &docker1SignedManifest{common: c, SignedManifest: mOrig}
	case mediatype.Docker2Manifest:
		var mOrig schema2.Manifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &docker2Manifest{common: c, Manifest: mOrig}
	case mediatype.Docker2ManifestList:
		var mOrig schema2.ManifestList
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &docker2ManifestList{common: c, ManifestList: mOrig}
	case mediatype.OCI1Manifest:
		var mOrig v1.Manifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &oci1Manifest{common: c, Manifest: mOrig}
	case mediatype.OCI1ManifestList:
		var mOrig v1.Index
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &oci1Index{common: c, Index: mOrig}
	case mediatype.OCI1Artifact:
		var mOrig v1.ArtifactManifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &oci1Artifact{common: c, ArtifactManifest: mOrig}
	default:
		return nil, fmt.Errorf("%w: \"%s\"", errs.ErrUnsupportedMediaType, c.desc.MediaType)
	}
	if err != nil {
		return nil, fmt.Errorf("error unmarshaling manifest for %s: %w", c.r.CommonName(), err)
	}
	// verify media type
	err = verifyMT(c.desc.MediaType, mt)
	if err != nil {
		return nil, err
	}
	// verify digest didn't change
	if origDigest != "" && origDigest != c.desc.Digest {
		return nil, fmt.Errorf("manifest digest mismatch, expected %s, computed %s", origDigest, c.desc.Digest)
	}
	return m, nil
}

Fixed code:
file_path: types/manifest/manifest.go
snippet: func New(opts ...Opts) (Manifest, error) {
	mc := manifestConfig{}
	for _, opt := range opts {
		opt(&mc)
	}
	c := common{
		r:         mc.r,
		desc:      mc.desc,
		rawBody:   mc.raw,
		rawHeader: mc.header,
	}
	if c.r.Digest != "" && c.desc.Digest == "" {
		dig, err := digest.Parse(c.r.Digest)
		if err != nil {
			return nil, fmt.Errorf("failed to parse digest from ref: %w", err)
		}
		c.desc.Digest = dig
	}
	// extract fields from header where available
	if mc.header != nil {
		if c.desc.MediaType == "" {
			c.desc.MediaType = mediatype.Base(mc.header.Get("Content-Type"))
		}
		if c.desc.Size == 0 {
			cl, _ := strconv.Atoi(mc.header.Get("Content-Length"))
			c.desc.Size = int64(cl)
		}
		if c.desc.Digest == "" {
			c.desc.Digest, _ = digest.Parse(mc.header.Get("Docker-Content-Digest"))
		}
		c.setRateLimit(mc.header)
	}
	if mc.orig != nil {
		return fromOrig(c, mc.orig)
	}
	return fromCommon(c)
}

file_path: types/manifest/manifest.go
snippet: func fromOrig(c common, orig interface{}) (Manifest, error) {
	var mt string
	var m Manifest
	origDigest := c.desc.Digest

	mj, err := json.Marshal(orig)
	if err != nil {
		return nil, err
	}
	c.manifSet = true
	if len(c.rawBody) == 0 {
		c.rawBody = mj
	}
	if _, ok := orig.(schema1.SignedManifest); !ok {
		c.desc.Digest = c.desc.DigestAlgo().FromBytes(mj)
	}
	if c.desc.Size == 0 {
		c.desc.Size = int64(len(mj))
	}
	// create manifest based on type
	switch mOrig := orig.(type) {
	case schema1.Manifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker1Manifest
		m = &docker1Manifest{
			common:   c,
			Manifest: mOrig,
		}
	case schema1.SignedManifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker1ManifestSigned
		// recompute digest on the canonical data
		c.desc.Digest = c.desc.DigestAlgo().FromBytes(mOrig.Canonical)
		m = &docker1SignedManifest{
			common:         c,
			SignedManifest: mOrig,
		}
	case schema2.Manifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker2Manifest
		m = &docker2Manifest{
			common:   c,
			Manifest: mOrig,
		}
	case schema2.ManifestList:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.Docker2ManifestList
		m = &docker2ManifestList{
			common:       c,
			ManifestList: mOrig,
		}
	case v1.Manifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.OCI1Manifest
		m = &oci1Manifest{
			common:   c,
			Manifest: mOrig,
		}
	case v1.Index:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.OCI1ManifestList
		m = &oci1Index{
			common: c,
			Index:  orig.(v1.Index),
		}
	case v1.ArtifactManifest:
		mt = mOrig.MediaType
		c.desc.MediaType = mediatype.OCI1Artifact
		m = &oci1Artifact{
			common:           c,
			ArtifactManifest: mOrig,
		}
	default:
		return nil, fmt.Errorf("unsupported type to convert to a manifest: %T", orig)
	}
	// verify media type
	err = verifyMT(c.desc.MediaType, mt)
	if err != nil {
		return nil, err
	}
	// verify digest didn't change
	if origDigest != "" && origDigest != c.desc.Digest {
		return nil, fmt.Errorf("manifest digest mismatch, expected %s, computed %s%.0w", origDigest, c.desc.Digest, errs.ErrDigestMismatch)
	}
	return m, nil
}

file_path: types/manifest/manifest.go
snippet: func fromCommon(c common) (Manifest, error) {
	var err error
	var m Manifest
	var mt string
	origDigest := c.desc.Digest
	// extract common data from from rawBody
	if len(c.rawBody) > 0 {
		c.manifSet = true
		// extract media type from body, either explicitly or with duck typing
		if c.desc.MediaType == "" {
			mt := struct {
				MediaType     string                  `json:"mediaType,omitempty"`
				SchemaVersion int                     `json:"schemaVersion,omitempty"`
				Signatures    []interface{}           `json:"signatures,omitempty"`
				Manifests     []descriptor.Descriptor `json:"manifests,omitempty"`
				Layers        []descriptor.Descriptor `json:"layers,omitempty"`
			}{}
			err = json.Unmarshal(c.rawBody, &mt)
			if mt.MediaType != "" {
				c.desc.MediaType = mt.MediaType
			} else if mt.SchemaVersion == 1 && len(mt.Signatures) > 0 {
				c.desc.MediaType = mediatype.Docker1ManifestSigned
			} else if mt.SchemaVersion == 1 {
				c.desc.MediaType = mediatype.Docker1Manifest
			} else if len(mt.Manifests) > 0 {
				if strings.HasPrefix(mt.Manifests[0].MediaType, "application/vnd.docker.") {
					c.desc.MediaType = mediatype.Docker2ManifestList
				} else {
					c.desc.MediaType = mediatype.OCI1ManifestList
				}
			} else if len(mt.Layers) > 0 {
				if strings.HasPrefix(mt.Layers[0].MediaType, "application/vnd.docker.") {
					c.desc.MediaType = mediatype.Docker2Manifest
				} else {
					c.desc.MediaType = mediatype.OCI1Manifest
				}
			}
		}
		// compute digest
		if c.desc.MediaType != mediatype.Docker1ManifestSigned {
			d := c.desc.DigestAlgo().FromBytes(c.rawBody)
			c.desc.Digest = d
			c.desc.Size = int64(len(c.rawBody))
		}
	}
	switch c.desc.MediaType {
	case mediatype.Docker1Manifest:
		var mOrig schema1.Manifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &docker1Manifest{common: c, Manifest: mOrig}
	case mediatype.Docker1ManifestSigned:
		var mOrig schema1.SignedManifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
			d := c.desc.DigestAlgo().FromBytes(mOrig.Canonical)
			c.desc.Digest = d
			c.desc.Size = int64(len(mOrig.Canonical))
		}
		m = &docker1SignedManifest{common: c, SignedManifest: mOrig}
	case mediatype.Docker2Manifest:
		var mOrig schema2.Manifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &docker2Manifest{common: c, Manifest: mOrig}
	case mediatype.Docker2ManifestList:
		var mOrig schema2.ManifestList
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &docker2ManifestList{common: c, ManifestList: mOrig}
	case mediatype.OCI1Manifest:
		var mOrig v1.Manifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &oci1Manifest{common: c, Manifest: mOrig}
	case mediatype.OCI1ManifestList:
		var mOrig v1.Index
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &oci1Index{common: c, Index: mOrig}
	case mediatype.OCI1Artifact:
		var mOrig v1.ArtifactManifest
		if len(c.rawBody) > 0 {
			err = json.Unmarshal(c.rawBody, &mOrig)
			mt = mOrig.MediaType
		}
		m = &oci1Artifact{common: c, ArtifactManifest: mOrig}
	default:
		return nil, fmt.Errorf("%w: \"%s\"", errs.ErrUnsupportedMediaType, c.desc.MediaType)
	}
	if err != nil {
		return nil, fmt.Errorf("error unmarshaling manifest for %s: %w", c.r.CommonName(), err)
	}
	// verify media type
	err = verifyMT(c.desc.MediaType, mt)
	if err != nil {
		return nil, err
	}
	// verify digest didn't change
	if origDigest != "" && origDigest != c.desc.Digest {
		return nil, fmt.Errorf("manifest digest mismatch, expected %s, computed %s%.0w", origDigest, c.desc.Digest, errs.ErrDigestMismatch)
	}
	return m, nil
}