Skip to content

Commit 107f4f0

Browse files
committed
casext: add handling for empty JSON media-type
While this is a fairly hypothetical problem, the spec explicitly defines what "application/vnd.oci.empty.v1+json" should look like and so we should validate it. Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
1 parent 7303c77 commit 107f4f0

File tree

6 files changed

+129
-0
lines changed

6 files changed

+129
-0
lines changed

internal/errors.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ import (
2525
// ErrUnimplemented is returned as a source error for umoci features that are
2626
// not yet implemented.
2727
var ErrUnimplemented = errors.New("unimplemented umoci feature")
28+
29+
// ErrInvalidEmptyJSON is returned from the mediatype parser if a descriptor
30+
// with the "application/vnd.oci.empty.v1+json" media-type has any value other
31+
// than "{}".
32+
var ErrInvalidEmptyJSON = errors.New("empty json blob is invalid")

oci/casext/blob.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Blob struct {
5454
// ispec.MediaTypeImageLayerNonDistributable => io.ReadCloser
5555
// ispec.MediaTypeImageLayerNonDistributableGzip => io.ReadCloser
5656
// ispec.MediaTypeImageConfig => ispec.Image
57+
// ispec.MediaTypeEmptyJSON => struct{}
5758
// unknown => io.ReadCloser
5859
Data any
5960
}

oci/casext/blob_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/stretchr/testify/assert"
2727
"github.com/stretchr/testify/require"
2828

29+
"github.com/opencontainers/umoci/internal"
2930
"github.com/opencontainers/umoci/pkg/hardening"
3031
)
3132

@@ -50,6 +51,16 @@ func TestDescriptorEmbeddedData(t *testing.T) {
5051
descriptor: ispec.DescriptorEmptyJSON,
5152
expectedData: struct{}{},
5253
},
54+
{
55+
name: "EmptyJSON-BadData",
56+
descriptor: ispec.Descriptor{
57+
MediaType: ispec.MediaTypeEmptyJSON,
58+
Digest: "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
59+
Size: 4,
60+
Data: []byte("null"),
61+
},
62+
expectedErr: internal.ErrInvalidEmptyJSON,
63+
},
5364
{
5465
name: "BadDigest",
5566
descriptor: ispec.Descriptor{

oci/casext/mediatype/parse.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package mediatype
2020

2121
import (
22+
"bytes"
2223
"encoding/json"
2324
"errors"
2425
"fmt"
@@ -27,6 +28,8 @@ import (
2728
"sync"
2829

2930
ispec "github.com/opencontainers/image-spec/specs-go/v1"
31+
32+
"github.com/opencontainers/umoci/internal"
3033
)
3134

3235
// ErrNilReader is returned by the parsers in this package when they are called
@@ -206,11 +209,35 @@ func manifestParser(rdr io.Reader) (any, error) {
206209
return manifest.Manifest, nil
207210
}
208211

212+
// emptyJSONParser only parses "application/vnd.oci.empty.v1+json" and
213+
// validates that it is actually "{}".
214+
func emptyJSONParser(rdr io.Reader) (any, error) {
215+
if rdr == nil {
216+
// must not return a nil interface{}
217+
return struct{}{}, ErrNilReader
218+
}
219+
220+
// The only valid value for this blob.
221+
const emptyJSON = `{}`
222+
223+
// Try to read at least one more byte than emptyJSON so if there is some
224+
// trailing data we will error out without needing to read any more.
225+
data, err := io.ReadAll(io.LimitReader(rdr, int64(len(emptyJSON))+1))
226+
if err != nil {
227+
return nil, err
228+
}
229+
if !bytes.Equal(data, []byte(emptyJSON)) {
230+
return nil, internal.ErrInvalidEmptyJSON
231+
}
232+
return struct{}{}, nil
233+
}
234+
209235
// Register the core image-spec types.
210236
func init() {
211237
RegisterParser(ispec.MediaTypeDescriptor, JSONParser[ispec.Descriptor])
212238
RegisterParser(ispec.MediaTypeImageIndex, indexParser)
213239
RegisterParser(ispec.MediaTypeImageConfig, JSONParser[ispec.Image])
240+
RegisterParser(ispec.MediaTypeEmptyJSON, emptyJSONParser)
214241

215242
RegisterTarget(ispec.MediaTypeImageManifest)
216243
RegisterParser(ispec.MediaTypeImageManifest, manifestParser)

oci/casext/mediatype/parse_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
/*
3+
* umoci: Umoci Modifies Open Containers' Images
4+
* Copyright (C) 2016-2025 SUSE LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package mediatype
20+
21+
import (
22+
"bytes"
23+
"testing"
24+
25+
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
27+
28+
"github.com/opencontainers/umoci/internal"
29+
)
30+
31+
// TODO: Add more parsing tests.
32+
33+
func TestParseEmptyJSON(t *testing.T) {
34+
for _, test := range []struct {
35+
name string
36+
data []byte
37+
expectedBlob any
38+
expectedErr error
39+
}{
40+
{
41+
name: "Good",
42+
data: []byte("{}"),
43+
expectedBlob: struct{}{},
44+
},
45+
{
46+
name: "Bad-Empty",
47+
data: []byte{},
48+
expectedErr: internal.ErrInvalidEmptyJSON,
49+
},
50+
{
51+
name: "Bad-Short",
52+
data: []byte(`0`),
53+
expectedErr: internal.ErrInvalidEmptyJSON,
54+
},
55+
{
56+
name: "Bad-Suffix",
57+
data: []byte("{}\n"),
58+
expectedErr: internal.ErrInvalidEmptyJSON,
59+
},
60+
{
61+
name: "Bad",
62+
data: []byte("The quick brown fox jumps over the lazy dog.\n"),
63+
expectedErr: internal.ErrInvalidEmptyJSON,
64+
},
65+
} {
66+
t.Run(test.name, func(t *testing.T) {
67+
got, err := emptyJSONParser(bytes.NewBuffer(test.data))
68+
require.ErrorIsf(t, err, test.expectedErr, "emptyJSONParser(%q)", string(test.data))
69+
assert.Equalf(t, test.expectedBlob, got, "emptyJSONParser(%q)", string(test.data))
70+
})
71+
}
72+
}

oci/casext/verified_blob.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
ispec "github.com/opencontainers/image-spec/specs-go/v1"
2929

30+
"github.com/opencontainers/umoci/internal"
3031
"github.com/opencontainers/umoci/pkg/hardening"
3132
)
3233

@@ -42,6 +43,18 @@ func (e Engine) GetVerifiedBlob(ctx context.Context, descriptor ispec.Descriptor
4243
if descriptor.Size < 0 {
4344
return nil, fmt.Errorf("invalid descriptor: %w", errInvalidDescriptorSize)
4445
}
46+
// The empty blob descriptor only has one valid value so we should validate
47+
// it before allowing it to be opened.
48+
if descriptor.MediaType == ispec.MediaTypeEmptyJSON {
49+
if descriptor.Digest != ispec.DescriptorEmptyJSON.Digest ||
50+
descriptor.Size != ispec.DescriptorEmptyJSON.Size {
51+
return nil, fmt.Errorf("invalid descriptor: %w", internal.ErrInvalidEmptyJSON)
52+
}
53+
if descriptor.Data != nil &&
54+
!bytes.Equal(descriptor.Data, ispec.DescriptorEmptyJSON.Data) {
55+
return nil, fmt.Errorf("invalid descriptor: %w", internal.ErrInvalidEmptyJSON)
56+
}
57+
}
4558
// Embedded data.
4659
if descriptor.Data != nil {
4760
// If the digest is small enough to fit in the descriptor, we can

0 commit comments

Comments
 (0)