Skip to content

Commit 114f2af

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 based on 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 seems like a bad idea in general. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent e7060df commit 114f2af

File tree

2 files changed

+38
-60
lines changed

2 files changed

+38
-60
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: 15 additions & 44 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,22 @@ 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+
require.Error(t, err, "io.Copy (with ExpectedSize < 0) should fail")
82+
require.Zero(t, n, "io.Copy (with ExpectedSize < 0) should read nothing")
83+
assert.ErrorIs(t, err, ErrInvalidExpectedSize, "io.Copy (with ExpectedSize < 0) should fail")
84+
85+
// Bad ExpectedSize should read no data.
86+
assert.EqualValues(t, size, buffer.Len(), "io.Copy (with ExpectedSize < 0) should not have read any data")
8287

8388
// And on close we shouldn't get an error either.
8489
err = verifiedReader.Close()
85-
require.NoError(t, err, "digest (size ignored) should be correct on Close")
90+
require.Error(t, err, "Close (with ExpectedSize < 0) should fail")
91+
assert.ErrorIs(t, err, ErrInvalidExpectedSize, "Close (with ExpectedSize < 0) should fail")
92+
93+
// Bad ExpectedSize should read no data.
94+
assert.EqualValues(t, size, buffer.Len(), "close (with ExpectedSize < 0) should not have read any data")
8695
})
8796
}
8897
}
@@ -100,7 +109,7 @@ func TestValidTrailing(t *testing.T) {
100109
verifiedReader := &VerifiedReadCloser{
101110
Reader: io.NopCloser(buffer),
102111
ExpectedDigest: expectedDigest,
103-
ExpectedSize: -1,
112+
ExpectedSize: int64(size),
104113
}
105114

106115
// Read *half* of the bytes, leaving some remaining. We should get
@@ -147,44 +156,6 @@ func TestInvalidDigest(t *testing.T) {
147156
}
148157
}
149158

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-
188159
func TestInvalidSize_LongBuffer(t *testing.T) {
189160
for size := 1; size <= 16384; size *= 2 {
190161
for delta := 1; delta-1 <= size/2; delta *= 2 {

0 commit comments

Comments
 (0)