diff --git a/manifest/manifestlist/manifestlist.go b/manifest/manifestlist/manifestlist.go index 55c0224e..9cc2fc15 100644 --- a/manifest/manifestlist/manifestlist.go +++ b/manifest/manifestlist/manifestlist.go @@ -54,6 +54,9 @@ func init() { } imageIndexFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { + if err := validateIndex(b); err != nil { + return nil, distribution.Descriptor{}, err + } m := new(DeserializedManifestList) err := m.UnmarshalJSON(b) if err != nil { @@ -221,3 +224,23 @@ func (m DeserializedManifestList) Payload() (string, []byte, error) { return mediaType, m.canonical, nil } + +// unknownDocument represents a manifest, manifest list, or index that has not +// yet been validated +type unknownDocument struct { + Config interface{} `json:"config,omitempty"` + Layers interface{} `json:"layers,omitempty"` +} + +// validateIndex returns an error if the byte slice is invalid JSON or if it +// contains fields that belong to a manifest +func validateIndex(b []byte) error { + var doc unknownDocument + if err := json.Unmarshal(b, &doc); err != nil { + return err + } + if doc.Config != nil || doc.Layers != nil { + return errors.New("index: expected index but found manifest") + } + return nil +} diff --git a/manifest/manifestlist/manifestlist_test.go b/manifest/manifestlist/manifestlist_test.go index 860dca51..efbb72e8 100644 --- a/manifest/manifestlist/manifestlist_test.go +++ b/manifest/manifestlist/manifestlist_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/distribution/distribution/v3" + "github.com/distribution/distribution/v3/manifest/ocischema" + v1 "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -327,3 +329,33 @@ func TestMediaTypes(t *testing.T) { mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex, false) mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex+"XXX", true) } + +func TestValidateManifest(t *testing.T) { + manifest := ocischema.Manifest{ + Config: distribution.Descriptor{Size: 1}, + Layers: []distribution.Descriptor{{Size: 2}}, + } + index := ManifestList{ + Manifests: []ManifestDescriptor{ + {Descriptor: distribution.Descriptor{Size: 3}}, + }, + } + t.Run("valid", func(t *testing.T) { + b, err := json.Marshal(index) + if err != nil { + t.Fatal("unexpected error marshaling index", err) + } + if err := validateIndex(b); err != nil { + t.Error("index should be valid", err) + } + }) + t.Run("invalid", func(t *testing.T) { + b, err := json.Marshal(manifest) + if err != nil { + t.Fatal("unexpected error marshaling manifest", err) + } + if err := validateIndex(b); err == nil { + t.Error("manifest should not be valid") + } + }) +} diff --git a/manifest/ocischema/manifest.go b/manifest/ocischema/manifest.go index 968de6e3..e76e4907 100644 --- a/manifest/ocischema/manifest.go +++ b/manifest/ocischema/manifest.go @@ -22,6 +22,9 @@ var ( func init() { ocischemaFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { + if err := validateManifest(b); err != nil { + return nil, distribution.Descriptor{}, err + } m := new(DeserializedManifest) err := m.UnmarshalJSON(b) if err != nil { @@ -122,3 +125,22 @@ func (m *DeserializedManifest) MarshalJSON() ([]byte, error) { func (m DeserializedManifest) Payload() (string, []byte, error) { return v1.MediaTypeImageManifest, m.canonical, nil } + +// unknownDocument represents a manifest, manifest list, or index that has not +// yet been validated +type unknownDocument struct { + Manifests interface{} `json:"manifests,omitempty"` +} + +// validateManifest returns an error if the byte slice is invalid JSON or if it +// contains fields that belong to a index +func validateManifest(b []byte) error { + var doc unknownDocument + if err := json.Unmarshal(b, &doc); err != nil { + return err + } + if doc.Manifests != nil { + return errors.New("ocimanifest: expected manifest but found index") + } + return nil +} diff --git a/manifest/ocischema/manifest_test.go b/manifest/ocischema/manifest_test.go index 87a6479e..03b9edcb 100644 --- a/manifest/ocischema/manifest_test.go +++ b/manifest/ocischema/manifest_test.go @@ -8,6 +8,8 @@ import ( "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/manifest" + "github.com/distribution/distribution/v3/manifest/manifestlist" + v1 "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -182,3 +184,33 @@ func TestMediaTypes(t *testing.T) { mediaTypeTest(t, v1.MediaTypeImageManifest, false) mediaTypeTest(t, v1.MediaTypeImageManifest+"XXX", true) } + +func TestValidateManifest(t *testing.T) { + manifest := Manifest{ + Config: distribution.Descriptor{Size: 1}, + Layers: []distribution.Descriptor{{Size: 2}}, + } + index := manifestlist.ManifestList{ + Manifests: []manifestlist.ManifestDescriptor{ + {Descriptor: distribution.Descriptor{Size: 3}}, + }, + } + t.Run("valid", func(t *testing.T) { + b, err := json.Marshal(manifest) + if err != nil { + t.Fatal("unexpected error marshaling manifest", err) + } + if err := validateManifest(b); err != nil { + t.Error("manifest should be valid", err) + } + }) + t.Run("invalid", func(t *testing.T) { + b, err := json.Marshal(index) + if err != nil { + t.Fatal("unexpected error marshaling index", err) + } + if err := validateManifest(b); err == nil { + t.Error("index should not be valid") + } + }) +}