Skip to content

Commit 38fc012

Browse files
committed
pkg: hardening: disallow negative ExpectedSize
VerifiedReadCloser previously would allow for negative ExpectedSize to disable the size checking features added in commit ad66299 ("pkg: hardening: expand to verify descriptor length"). This was added partially because a somewhat overly-permissive reading of the discussion in opencontainers/image-spec#153 (which was finally clarified in opencontainers/image-spec#1285), but was also necessary for some users of VerifiedReadCloser that did not really know the proper blob size. We have now adjusted all of those callers, so there is no longer any reason to continue supporting this. Unknown sizes are a classic DoS vector, so allowing them seems like a bad idea in general. We might need to adjust this if/when umoci grows OCI distribution-spec support, but for now it isn't needed. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent 3037f87 commit 38fc012

File tree

2 files changed

+35
-59
lines changed

2 files changed

+35
-59
lines changed

pkg/hardening/verified_reader.go

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ import (
3333
// Exported errors for verification issues that occur during processing within
3434
// VerifiedReadCloser.
3535
var (
36-
ErrDigestMismatch = errors.New("verified reader digest mismatch")
37-
ErrSizeMismatch = errors.New("verified reader size mismatch")
36+
ErrDigestMismatch = errors.New("verified reader digest mismatch")
37+
ErrSizeMismatch = errors.New("verified reader size mismatch")
38+
ErrInvalidExpectedSize = errors.New("verified reader has invalid expected size")
3839
)
3940

4041
// VerifiedReadCloser is a basic io.ReadCloser which allows for simple
@@ -87,21 +88,25 @@ func (v *VerifiedReadCloser) isNoop() bool {
8788
innerV.ExpectedSize == v.ExpectedSize
8889
}
8990

91+
func (v *VerifiedReadCloser) check() error {
92+
if v.ExpectedSize < 0 {
93+
return fmt.Errorf("%w: expected size must be non-negative", ErrInvalidExpectedSize)
94+
}
95+
return nil
96+
}
97+
9098
func (v *VerifiedReadCloser) verify(nilErr error) error {
9199
// Digest mismatch (always takes precedence)?
92100
if actualDigest := v.digester.Digest(); actualDigest != v.ExpectedDigest {
93101
return fmt.Errorf("expected %s not %s: %w", v.ExpectedDigest, actualDigest, ErrDigestMismatch)
94102
}
95-
// Do we need to check the size for mismatches?
96-
if v.ExpectedSize >= 0 {
97-
switch {
98-
// Not enough bytes in the stream.
99-
case v.currentSize < v.ExpectedSize:
100-
return fmt.Errorf("expected %d bytes (only %d bytes in stream): %w", v.ExpectedSize, v.currentSize, ErrSizeMismatch)
101-
// We don't read the entire blob, so the message needs to be slightly adjusted.
102-
case v.currentSize > v.ExpectedSize:
103-
return fmt.Errorf("expected %d bytes (extra bytes in stream): %w", v.ExpectedSize, ErrSizeMismatch)
104-
}
103+
switch {
104+
// Not enough bytes in the stream.
105+
case v.currentSize < v.ExpectedSize:
106+
return fmt.Errorf("expected %d bytes (only %d bytes in stream): %w", v.ExpectedSize, v.currentSize, ErrSizeMismatch)
107+
// We don't read the entire blob, so the message needs to be slightly adjusted.
108+
case v.currentSize > v.ExpectedSize:
109+
return fmt.Errorf("expected %d bytes (extra bytes in stream): %w", v.ExpectedSize, ErrSizeMismatch)
105110
}
106111
// Forward the provided error.
107112
return nilErr
@@ -111,14 +116,13 @@ func (v *VerifiedReadCloser) verify(nilErr error) error {
111116
// EOF. Make sure that you always check for EOF and read-to-the-end for all
112117
// files.
113118
func (v *VerifiedReadCloser) Read(p []byte) (n int, err error) {
119+
if err := v.check(); err != nil {
120+
return 0, err
121+
}
114122
// Make sure we don't read after v.ExpectedSize has been passed.
115123
err = io.EOF
116124
left := v.ExpectedSize - v.currentSize
117125
switch {
118-
// ExpectedSize has been disabled.
119-
case v.ExpectedSize < 0:
120-
n, err = v.Reader.Read(p)
121-
122126
// We still have something left to read.
123127
case left > 0:
124128
if int64(len(p)) > left {
@@ -180,6 +184,9 @@ func (v *VerifiedReadCloser) sourceName() string {
180184
// Close is a wrapper around VerifiedReadCloser.Reader, but with a digest check
181185
// which will return an error if the underlying Close() didn't.
182186
func (v *VerifiedReadCloser) Close() error {
187+
if err := v.check(); err != nil {
188+
return err
189+
}
183190
// Consume any remaining bytes to make sure that we've actually read to the
184191
// end of the stream. VerifiedReadCloser.Read will not read past
185192
// ExpectedSize+1, so we don't need to add a limit here.

pkg/hardening/verified_reader_test.go

Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func TestValid(t *testing.T) {
6060
}
6161
}
6262

63-
func TestValidIgnoreLength(t *testing.T) {
63+
func TestNegativeExpectedSize(t *testing.T) {
6464
for size := 1; size <= 16384; size *= 2 {
6565
t.Run(fmt.Sprintf("size:%d", size), func(t *testing.T) {
6666
// Fill buffer with random data.
@@ -76,13 +76,20 @@ func TestValidIgnoreLength(t *testing.T) {
7676
ExpectedSize: -1,
7777
}
7878

79-
// Make sure if we copy-to-EOF we get no errors.
80-
_, err = io.Copy(io.Discard, verifiedReader)
81-
require.NoError(t, err, "digest (size ignored) should be correct on EOF")
79+
// Make sure that negative ExpectedSize always fails.
80+
n, err := io.Copy(io.Discard, verifiedReader)
81+
assert.ErrorIs(t, err, ErrInvalidExpectedSize, "io.Copy (with ExpectedSize < 0) should fail") //nolint:testifylint // assert.*Error* makes more sense
82+
assert.Zero(t, n, "io.Copy (with ExpectedSize < 0) should read nothing")
83+
84+
// Bad ExpectedSize should read no data.
85+
assert.Equal(t, size, buffer.Len(), "io.Copy (with ExpectedSize < 0) should not have read any data")
8286

8387
// And on close we shouldn't get an error either.
8488
err = verifiedReader.Close()
85-
require.NoError(t, err, "digest (size ignored) should be correct on Close")
89+
assert.ErrorIs(t, err, ErrInvalidExpectedSize, "Close (with ExpectedSize < 0) should fail") //nolint:testifylint // assert.*Error* makes more sense
90+
91+
// Bad ExpectedSize should read no data.
92+
assert.Equal(t, size, buffer.Len(), "close (with ExpectedSize < 0) should not have read any data")
8693
})
8794
}
8895
}
@@ -147,44 +154,6 @@ func TestInvalidDigest(t *testing.T) {
147154
}
148155
}
149156

150-
func TestInvalidDigest_Trailing_NoExpectedSize(t *testing.T) {
151-
for size := 1; size <= 16384; size *= 2 {
152-
for delta := 1; delta-1 <= size/2; delta *= 2 {
153-
t.Run(fmt.Sprintf("size:%d_delta:%d", size, delta), func(t *testing.T) {
154-
// Fill buffer with random data.
155-
buffer := new(bytes.Buffer)
156-
_, err := io.CopyN(buffer, rand.Reader, int64(size))
157-
require.NoError(t, err, "fill buffer with random data")
158-
159-
// Generate a correct hash (for a shorter buffer), but don't
160-
// verify the size -- this is to make sure that we actually
161-
// read all the bytes.
162-
shortBuffer := buffer.Bytes()[:size-delta]
163-
expectedDigest := digest.SHA256.FromBytes(shortBuffer)
164-
verifiedReader := &VerifiedReadCloser{
165-
Reader: io.NopCloser(buffer),
166-
ExpectedDigest: expectedDigest,
167-
ExpectedSize: -1,
168-
}
169-
170-
// Read up to the end of the short buffer. We should get no
171-
// errors.
172-
_, err = io.CopyN(io.Discard, verifiedReader, int64(size-delta))
173-
require.NoErrorf(t, err, "should get no errors when reading %d (%d-%d) bytes", size-delta, size, delta)
174-
175-
// Check that the digest does actually match right now.
176-
verifiedReader.init()
177-
err = verifiedReader.verify(nil)
178-
require.NoError(t, err, "digest check should succeed at the point we finish the subset")
179-
180-
// On close we should get the error.
181-
err = verifiedReader.Close()
182-
assert.ErrorIs(t, err, ErrDigestMismatch, "digest should be invalid on Close") //nolint:testifylint // assert.*Error* makes more sense
183-
})
184-
}
185-
}
186-
}
187-
188157
func TestInvalidSize_LongBuffer(t *testing.T) {
189158
for size := 1; size <= 16384; size *= 2 {
190159
for delta := 1; delta-1 <= size/2; delta *= 2 {

0 commit comments

Comments
 (0)