Skip to content

Commit 3037f87

Browse files
committed
oci: casext: explicitly disallow negative-size descriptors
This was implicitly allowed by VerifiedReadCloser, and while we have closed that hole it's probably best to provide a more helpful error message. Not blocking this earlier was mostly due to a somewhat overly-permissive reading of the discussion in opencontainers/image-spec#153 which was finally clarified in opencontainers/image-spec#1285. Unknown sizes are a classic DoS vector, so allowing them (especially for descriptors where it makes little sense to have an unknown size) seems like a bad idea in general. Ref: opencontainers/image-spec#1285 Signed-off-by: Aleksa Sarai <[email protected]>
1 parent 5f31a52 commit 3037f87

File tree

3 files changed

+91
-0
lines changed

3 files changed

+91
-0
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919
the `manifests` entry to `null` (which was technically a violation of the
2020
specification, though such images cannot be pushed or interacted with outside
2121
of umoci).
22+
* Based on [some recent developments in the image-spec][image-spec#1285], umoci
23+
will now produce an error if it encounters descriptors with a negative size
24+
(this was a potential DoS vector previously) as well as a theoretical attack
25+
where an attacker would endlessly write to a blob (this would not be
26+
generally exploitable for images with descriptors).
2227

2328
### Changed ###
2429
* We now use `go:embed` to fill the version information of `umoci --version`,
@@ -33,6 +38,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3338
(and after that, we may choose to keep the `jq`-based validators as a
3439
double-check that our own validators are working correctly).
3540

41+
[image-spec#1285]: https://github.com/opencontainers/image-spec/pull/1285
3642
[docker-library/meta-scripts]: https://github.com/docker-library/meta-scripts
3743

3844
## [0.5.0] - 2025-05-21 ##

oci/casext/verified_blob.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,27 @@ package casext
2020

2121
import (
2222
"context"
23+
"errors"
24+
"fmt"
2325
"io"
2426

2527
ispec "github.com/opencontainers/image-spec/specs-go/v1"
2628

2729
"github.com/opencontainers/umoci/pkg/hardening"
2830
)
2931

32+
var errInvalidDescriptorSize = errors.New("descriptor size must not be negative")
33+
3034
// GetVerifiedBlob returns a VerifiedReadCloser for retrieving a blob from the
3135
// image, which the caller must Close() *and* read-to-EOF (checking the error
3236
// code of both). Returns ErrNotExist if the digest is not found, and
3337
// ErrBlobDigestMismatch on a mismatched blob digest. In addition, the reader
3438
// is limited to the descriptor.Size.
3539
func (e Engine) GetVerifiedBlob(ctx context.Context, descriptor ispec.Descriptor) (io.ReadCloser, error) {
40+
// Negative sizes are not permitted by the spec, and are a DoS vector.
41+
if descriptor.Size < 0 {
42+
return nil, fmt.Errorf("invalid descriptor: %w", errInvalidDescriptorSize)
43+
}
3644
reader, err := e.GetBlob(ctx, descriptor.Digest)
3745
return &hardening.VerifiedReadCloser{
3846
Reader: reader,

oci/casext/verified_blob_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 casext
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"io"
25+
"path/filepath"
26+
"testing"
27+
28+
ispec "github.com/opencontainers/image-spec/specs-go/v1"
29+
"github.com/stretchr/testify/assert"
30+
"github.com/stretchr/testify/require"
31+
32+
"github.com/opencontainers/umoci/oci/cas/dir"
33+
)
34+
35+
func TestGetVerifiedBlob(t *testing.T) {
36+
image := filepath.Join(t.TempDir(), "image")
37+
err := dir.Create(image)
38+
require.NoError(t, err)
39+
40+
engine, err := dir.Open(image)
41+
require.NoError(t, err)
42+
engineExt := NewEngine(engine)
43+
defer engine.Close() //nolint:errcheck
44+
45+
descMap := fakeSetupEngine(t, engineExt)
46+
assert.NotEmpty(t, descMap, "fakeSetupEngine descriptor map")
47+
48+
t.Run("InvalidSize", func(t *testing.T) {
49+
for idx, test := range descMap {
50+
test := test // copy iterator
51+
t.Run(fmt.Sprintf("Descriptor%.2d", idx+1), func(t *testing.T) {
52+
ctx := context.Background()
53+
desc := test.result
54+
55+
blob, err := engineExt.GetVerifiedBlob(ctx, desc)
56+
assert.NoError(t, err, "get verified blob (regular descriptor)") //nolint:testifylint // assert.*Error* makes more sense
57+
if assert.NotNil(t, blob, "get verified blob (regular descriptor)") {
58+
// Avoid "trailing data" log warnings on Close.
59+
_, _ = io.Copy(io.Discard, blob)
60+
_ = blob.Close()
61+
}
62+
63+
badDescriptor := ispec.Descriptor{
64+
MediaType: desc.MediaType,
65+
Digest: desc.Digest,
66+
Size: -1, // invalid!
67+
}
68+
69+
blob, err = engineExt.GetVerifiedBlob(ctx, badDescriptor)
70+
assert.ErrorIs(t, err, errInvalidDescriptorSize, "get verified blob (negative descriptor size)") //nolint:testifylint // assert.*Error* makes more sense
71+
if !assert.Nil(t, blob, "get verified blob (negative descriptor size)") {
72+
_ = blob.Close()
73+
}
74+
})
75+
}
76+
})
77+
}

0 commit comments

Comments
 (0)