Skip to content

Commit

Permalink
Merge pull request #302 from CosmWasm/code_max_param_289
Browse files Browse the repository at this point in the history
Reject wasm code exceeding limit
  • Loading branch information
alpe authored Nov 9, 2020
2 parents 41cf73d + 2efd962 commit fbd7168
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 147 deletions.
5 changes: 3 additions & 2 deletions x/wasm/internal/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestGenesisExportImport(t *testing.T) {
srcKeeper.importContractState(srcCtx, contractAddr, stateModels)
}
var wasmParams types.Params
f.Fuzz(&wasmParams)
f.NilChance(0).Fuzz(&wasmParams)
srcKeeper.setParams(srcCtx, wasmParams)

// export
Expand Down Expand Up @@ -369,7 +369,8 @@ func TestImportContractWithCodeHistoryReset(t *testing.T) {
"code_upload_access": {
"permission": "Everybody"
},
"instantiate_default_permission": "Everybody"
"instantiate_default_permission": "Everybody",
"max_wasm_code_size": 500000
},
"codes": [
{
Expand Down
32 changes: 26 additions & 6 deletions x/wasm/internal/keeper/ioutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)))
}

// 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)
}
38 changes: 26 additions & 12 deletions x/wasm/internal/keeper/ioutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -57,31 +64,38 @@ func TestUncompress(t *testing.T) {
src: wasmGzipped[:len(wasmGzipped)-5],
expError: io.ErrUnexpectedEOF,
},
"handle limit gzip output": {
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()
Expand Down
10 changes: 8 additions & 2 deletions x/wasm/internal/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ func (k Keeper) getInstantiateAccessConfig(ctx sdk.Context) types.AccessType {
return a
}

func (k Keeper) GetMaxWasmCodeSize(ctx sdk.Context) uint64 {
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
Expand All @@ -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())
}
Expand All @@ -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())
}
Expand Down
1 change: 1 addition & 0 deletions x/wasm/internal/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func TestCreateStoresInstantiatePermission(t *testing.T) {
keeper.setParams(ctx, types.Params{
CodeUploadAccess: types.AllowEverybody,
InstantiateDefaultPermission: spec.srcPermission,
MaxWasmCodeSize: types.DefaultMaxWasmCodeSize,
})
fundAccounts(t, ctx, accKeeper, bankKeeper, myAddr, deposit)

Expand Down
24 changes: 20 additions & 4 deletions x/wasm/internal/keeper/proposal_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import (
func TestStoreCodeProposal(t *testing.T) {
ctx, keepers := CreateTestInput(t, false, "staking", nil, nil)
govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper
wasmKeeper.setParams(ctx, types.Params{CodeUploadAccess: types.AllowNobody, InstantiateDefaultPermission: types.AccessTypeNobody})
wasmKeeper.setParams(ctx, types.Params{
CodeUploadAccess: types.AllowNobody,
InstantiateDefaultPermission: types.AccessTypeNobody,
MaxWasmCodeSize: types.DefaultMaxWasmCodeSize,
})
wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm")
require.NoError(t, err)

Expand Down Expand Up @@ -55,7 +59,11 @@ func TestStoreCodeProposal(t *testing.T) {
func TestInstantiateProposal(t *testing.T) {
ctx, keepers := CreateTestInput(t, false, "staking", nil, nil)
govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper
wasmKeeper.setParams(ctx, types.Params{CodeUploadAccess: types.AllowNobody, InstantiateDefaultPermission: types.AccessTypeNobody})
wasmKeeper.setParams(ctx, types.Params{
CodeUploadAccess: types.AllowNobody,
InstantiateDefaultPermission: types.AccessTypeNobody,
MaxWasmCodeSize: types.DefaultMaxWasmCodeSize,
})

wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm")
require.NoError(t, err)
Expand Down Expand Up @@ -107,7 +115,11 @@ func TestInstantiateProposal(t *testing.T) {
func TestMigrateProposal(t *testing.T) {
ctx, keepers := CreateTestInput(t, false, "staking", nil, nil)
govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper
wasmKeeper.setParams(ctx, types.Params{CodeUploadAccess: types.AllowNobody, InstantiateDefaultPermission: types.AccessTypeNobody})
wasmKeeper.setParams(ctx, types.Params{
CodeUploadAccess: types.AllowNobody,
InstantiateDefaultPermission: types.AccessTypeNobody,
MaxWasmCodeSize: types.DefaultMaxWasmCodeSize,
})

wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm")
require.NoError(t, err)
Expand Down Expand Up @@ -236,7 +248,11 @@ func TestAdminProposals(t *testing.T) {
t.Run(msg, func(t *testing.T) {
ctx, keepers := CreateTestInput(t, false, "staking", nil, nil)
govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper
wasmKeeper.setParams(ctx, types.Params{CodeUploadAccess: types.AllowNobody, InstantiateDefaultPermission: types.AccessTypeNobody})
wasmKeeper.setParams(ctx, types.Params{
CodeUploadAccess: types.AllowNobody,
InstantiateDefaultPermission: types.AccessTypeNobody,
MaxWasmCodeSize: types.DefaultMaxWasmCodeSize,
})

codeInfoFixture := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode))
require.NoError(t, wasmKeeper.importCode(ctx, 1, codeInfoFixture, wasmCode))
Expand Down
19 changes: 19 additions & 0 deletions x/wasm/internal/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import (
const (
// DefaultParamspace for params keeper
DefaultParamspace = ModuleName
// DefaultMaxWasmCodeSize limit max bytes read to prevent gzip bombs
DefaultMaxWasmCodeSize = 600 * 1024
)

var ParamStoreKeyUploadAccess = []byte("uploadAccess")
var ParamStoreKeyInstantiateAccess = []byte("instantiateAccess")
var ParamStoreKeyMaxWasmCodeSize = []byte("maxWasmCodeSize")

var AllAccessTypes = []AccessType{
AccessTypeNobody,
Expand Down Expand Up @@ -95,6 +98,7 @@ func DefaultParams() Params {
return Params{
CodeUploadAccess: AllowEverybody,
InstantiateDefaultPermission: AccessTypeEverybody,
MaxWasmCodeSize: DefaultMaxWasmCodeSize,
}
}

Expand All @@ -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),
}
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit fbd7168

Please sign in to comment.