-
Notifications
You must be signed in to change notification settings - Fork 404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reject wasm code exceeding limit #302
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,20 +5,22 @@ import ( | |
"compress/gzip" | ||
"io" | ||
"io/ioutil" | ||
|
||
"github.com/CosmWasm/wasmd/x/wasm/internal/types" | ||
) | ||
|
||
// magic bytes to identify gzip. | ||
// See https://www.ietf.org/rfc/rfc1952.txt | ||
// and https://github.com/golang/go/blob/master/src/net/http/sniff.go#L186 | ||
var gzipIdent = []byte("\x1F\x8B\x08") | ||
|
||
// limit max bytes read to prevent gzip bombs | ||
const maxSize = 400 * 1024 | ||
|
||
// uncompress returns gzip uncompressed content or given src when not gzip. | ||
func uncompress(src []byte) ([]byte, error) { | ||
if len(src) < 3 { | ||
func uncompress(src []byte, limit uint64) ([]byte, error) { | ||
switch n := uint64(len(src)); { | ||
case n < 3: | ||
return src, nil | ||
case n > limit: | ||
return nil, types.ErrLimit | ||
} | ||
if !bytes.Equal(gzipIdent, src[0:3]) { | ||
return src, nil | ||
|
@@ -28,6 +30,24 @@ func uncompress(src []byte) ([]byte, error) { | |
return nil, err | ||
} | ||
zr.Multistream(false) | ||
defer zr.Close() | ||
return ioutil.ReadAll(LimitReader(zr, int64(limit))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means if the total size >= limit we will get the types.ErrLimit error. However, we want total_size > limit. |
||
} | ||
|
||
// LimitReader returns a Reader that reads from r | ||
// but stops with types.ErrLimit after n bytes. | ||
// The underlying implementation is a *io.LimitedReader. | ||
func LimitReader(r io.Reader, n int64) io.Reader { | ||
return &LimitedReader{r: &io.LimitedReader{R: r, N: n}} | ||
} | ||
|
||
type LimitedReader struct { | ||
r *io.LimitedReader | ||
} | ||
|
||
return ioutil.ReadAll(io.LimitReader(zr, maxSize)) | ||
func (l *LimitedReader) Read(p []byte) (n int, err error) { | ||
if l.r.N <= 0 { | ||
return 0, types.ErrLimit | ||
} | ||
return l.r.Read(p) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ReadAll keeps reading until one read returns Update: checked ioutil and bytes.Buffer. They only stop when it hits an error. If the error is EOF, then it stops and return data with no error, otherwise, it returns the error. Seems fine, just note this returns an error if the input is the exact size of n. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe, so the read, then check if it overflowed? Or maybe I don't fully understand how LimitedReader works. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import ( | |
"strings" | ||
"testing" | ||
|
||
"github.com/CosmWasm/wasmd/x/wasm/internal/types" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
@@ -20,6 +21,8 @@ func TestUncompress(t *testing.T) { | |
wasmGzipped, err := ioutil.ReadFile("./testdata/hackatom.wasm.gzip") | ||
require.NoError(t, err) | ||
|
||
const maxSize = 400_000 | ||
|
||
specs := map[string]struct { | ||
src []byte | ||
expError error | ||
|
@@ -41,9 +44,13 @@ func TestUncompress(t *testing.T) { | |
src: []byte{0x1, 0x2}, | ||
expResult: []byte{0x1, 0x2}, | ||
}, | ||
"handle big input slice": { | ||
src: []byte(strings.Repeat("a", maxSize+1)), | ||
expResult: []byte(strings.Repeat("a", maxSize+1)), | ||
"handle input slice exceeding limit": { | ||
src: []byte(strings.Repeat("a", maxSize+1)), | ||
expError: types.ErrLimit, | ||
}, | ||
"handle input slice at limit": { | ||
src: []byte(strings.Repeat("a", maxSize)), | ||
expResult: []byte(strings.Repeat("a", maxSize)), | ||
}, | ||
"handle gzip identifier only": { | ||
src: gzipIdent, | ||
|
@@ -57,31 +64,38 @@ func TestUncompress(t *testing.T) { | |
src: wasmGzipped[:len(wasmGzipped)-5], | ||
expError: io.ErrUnexpectedEOF, | ||
}, | ||
"handle limit gzip output": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, this passes? That means it can return The test cases (both limit and limit+1 gzips) pass. So I can approve it. Just seems there is some possible way something can slip through. |
||
src: asGzip(bytes.Repeat([]byte{0x1}, maxSize)), | ||
expResult: bytes.Repeat([]byte{0x1}, maxSize), | ||
}, | ||
"handle big gzip output": { | ||
src: asGzip(strings.Repeat("a", maxSize+1)), | ||
expError: io.ErrUnexpectedEOF, | ||
src: asGzip(bytes.Repeat([]byte{0x1}, maxSize+1)), | ||
expError: types.ErrLimit, | ||
}, | ||
"handle other big gzip output": { | ||
src: asGzip(strings.Repeat("a", 2*maxSize)), | ||
expError: io.ErrUnexpectedEOF, | ||
src: asGzip(bytes.Repeat([]byte{0x1}, 2*maxSize)), | ||
expError: types.ErrLimit, | ||
}, | ||
} | ||
for msg, spec := range specs { | ||
t.Run(msg, func(t *testing.T) { | ||
r, err := uncompress(spec.src) | ||
require.True(t, errors.Is(spec.expError, err), "exp %+v got %+v", spec.expError, err) | ||
r, err := uncompress(spec.src, maxSize) | ||
require.True(t, errors.Is(spec.expError, err), "exp %v got %+v", spec.expError, err) | ||
if spec.expError != nil { | ||
return | ||
} | ||
assert.Equal(t, spec.expResult, r) | ||
}) | ||
} | ||
|
||
} | ||
|
||
func asGzip(src string) []byte { | ||
func asGzip(src []byte) []byte { | ||
var buf bytes.Buffer | ||
if _, err := io.Copy(gzip.NewWriter(&buf), strings.NewReader(src)); err != nil { | ||
zipper := gzip.NewWriter(&buf) | ||
if _, err := io.Copy(zipper, bytes.NewReader(src)); err != nil { | ||
panic(err) | ||
} | ||
if err := zipper.Close(); err != nil { | ||
panic(err) | ||
} | ||
return buf.Bytes() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,6 +110,12 @@ func (k Keeper) getInstantiateAccessConfig(ctx sdk.Context) types.AccessType { | |
return a | ||
} | ||
|
||
func (k Keeper) GetMaxWasmCodeSize(ctx sdk.Context) uint64 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, nice to add the parameter here - this is set in genesis, right? |
||
var a uint64 | ||
k.paramSpace.Get(ctx, types.ParamStoreKeyMaxWasmCodeSize, &a) | ||
return a | ||
} | ||
|
||
// GetParams returns the total set of wasm parameters. | ||
func (k Keeper) GetParams(ctx sdk.Context) types.Params { | ||
var params types.Params | ||
|
@@ -130,7 +136,7 @@ func (k Keeper) create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, | |
if !authZ.CanCreateCode(k.getUploadAccessConfig(ctx), creator) { | ||
return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not create code") | ||
} | ||
wasmCode, err = uncompress(wasmCode) | ||
wasmCode, err = uncompress(wasmCode, k.GetMaxWasmCodeSize(ctx)) | ||
if err != nil { | ||
return 0, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) | ||
} | ||
|
@@ -155,7 +161,7 @@ func (k Keeper) create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, | |
} | ||
|
||
func (k Keeper) importCode(ctx sdk.Context, codeID uint64, codeInfo types.CodeInfo, wasmCode []byte) error { | ||
wasmCode, err := uncompress(wasmCode) | ||
wasmCode, err := uncompress(wasmCode, k.GetMaxWasmCodeSize(ctx)) | ||
if err != nil { | ||
return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,10 +15,13 @@ import ( | |
const ( | ||
// DefaultParamspace for params keeper | ||
DefaultParamspace = ModuleName | ||
// DefaultMaxWasmCodeSize limit max bytes read to prevent gzip bombs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very nice updates here. 💯 |
||
DefaultMaxWasmCodeSize = 600 * 1024 | ||
) | ||
|
||
var ParamStoreKeyUploadAccess = []byte("uploadAccess") | ||
var ParamStoreKeyInstantiateAccess = []byte("instantiateAccess") | ||
var ParamStoreKeyMaxWasmCodeSize = []byte("maxWasmCodeSize") | ||
|
||
var AllAccessTypes = []AccessType{ | ||
AccessTypeNobody, | ||
|
@@ -95,6 +98,7 @@ func DefaultParams() Params { | |
return Params{ | ||
CodeUploadAccess: AllowEverybody, | ||
InstantiateDefaultPermission: AccessTypeEverybody, | ||
MaxWasmCodeSize: DefaultMaxWasmCodeSize, | ||
} | ||
} | ||
|
||
|
@@ -108,6 +112,7 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { | |
return paramtypes.ParamSetPairs{ | ||
paramtypes.NewParamSetPair(ParamStoreKeyUploadAccess, &p.CodeUploadAccess, validateAccessConfig), | ||
paramtypes.NewParamSetPair(ParamStoreKeyInstantiateAccess, &p.InstantiateDefaultPermission, validateAccessType), | ||
paramtypes.NewParamSetPair(ParamStoreKeyMaxWasmCodeSize, &p.MaxWasmCodeSize, validateMaxWasmCodeSize), | ||
} | ||
} | ||
|
||
|
@@ -119,6 +124,9 @@ func (p Params) ValidateBasic() error { | |
if err := validateAccessConfig(p.CodeUploadAccess); err != nil { | ||
return errors.Wrap(err, "upload access") | ||
} | ||
if err := validateMaxWasmCodeSize(p.MaxWasmCodeSize); err != nil { | ||
return errors.Wrap(err, "max wasm code size") | ||
} | ||
return nil | ||
} | ||
|
||
|
@@ -146,6 +154,17 @@ func validateAccessType(i interface{}) error { | |
return sdkerrors.Wrapf(ErrInvalid, "unknown type: %q", a) | ||
} | ||
|
||
func validateMaxWasmCodeSize(i interface{}) error { | ||
a, ok := i.(uint64) | ||
if !ok { | ||
return sdkerrors.Wrapf(ErrInvalid, "type: %T", i) | ||
} | ||
if a == 0 { | ||
return sdkerrors.Wrap(ErrInvalid, "must be greater 0") | ||
} | ||
return nil | ||
} | ||
|
||
func (v AccessConfig) ValidateBasic() error { | ||
switch v.Permission { | ||
case AccessTypeUndefined: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check also rejects compressed data > limit, which is smaller than limit when uncompressed. Is this intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good edge case!
This can only happen when gzip does not compress the data but adds it header on top. I guess that is very unlikely to happen and the workaround would be to send uncompressed data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. If found the code a bit confusing to review, because it is not very clear which parts deals with compressed and which with uncompressed data. Some checks operate on both.