From 5151e650c914b3130313e0f79862a19bde95955e Mon Sep 17 00:00:00 2001 From: mmsqe Date: Wed, 14 Jun 2023 15:40:58 +0800 Subject: [PATCH] add return limit for call (#269) --- CHANGELOG.md | 4 + gomod2nix.toml | 20 ++--- rpc/backend/call_tx.go | 4 + server/config/config.go | 7 ++ server/config/toml.go | 3 + server/flags/flags.go | 1 + .../integration_tests/configs/exploit.jsonnet | 11 +++ .../hardhat/contracts/TestExploitContract.sol | 10 +++ tests/integration_tests/test_exploit.py | 75 +++++++++++++++++++ tests/integration_tests/utils.py | 1 + 10 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 tests/integration_tests/configs/exploit.jsonnet create mode 100644 tests/integration_tests/hardhat/contracts/TestExploitContract.sol create mode 100644 tests/integration_tests/test_exploit.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 84fe644400..182d7b7fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +### Features + +* (rpc) [#1682](https://github.com/evmos/ethermint/pull/1682) Add config for maximum number of bytes returned from eth_call. + ### State Machine Breaking - (deps) [#1168](https://github.com/evmos/ethermint/pull/1716) Bump Cosmos-SDK to v0.46.11, Tendermint to v0.34.27, IAVL v0.19.5 and btcd to v0.23.4 diff --git a/gomod2nix.toml b/gomod2nix.toml index f5b91071dd..0e3d07aa46 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -409,8 +409,8 @@ schema = 3 version = "v0.9.1" hash = "sha256-YLGNrHHM+mN4ElW/XWuylOnFrA/VjSY+eBuC4LN//5c=" [mod."github.com/rs/cors"] - version = "v1.8.3" - hash = "sha256-VgVB4HKAhPSjNg96mIEUN1bt5ZQng8Fi3ZABy3CDWQE=" + version = "v1.9.0" + hash = "sha256-CNBCGXOydU6MIEZ0B5eXBjBm3smH2ryYHvgRJA2udBM=" [mod."github.com/rs/zerolog"] version = "v1.27.0" hash = "sha256-BxQtP2TROeSSpj9l1irocuSfxn55UL4ugzB/og7r8eE=" @@ -512,8 +512,8 @@ schema = 3 version = "v0.0.0-20230131160201-f062dba9d201" hash = "sha256-sxLT/VOe93v0h3miChJSHS9gscTZS/B71+390ju/e20=" [mod."golang.org/x/net"] - version = "v0.8.0" - hash = "sha256-2cOtqa7aJ5mn64kZ+8+PVjJ4uGbhpXTpC1vm/+iaZzM=" + version = "v0.9.0" + hash = "sha256-EG5GRDq282twyce8uugsDTjMz1pNn6zPcyVTZmSiJ14=" [mod."golang.org/x/oauth2"] version = "v0.4.0" hash = "sha256-Dj9wHbSbs0Ghr9Hef0hSfanaR8L0GShI18jGBT3yNn8=" @@ -521,14 +521,14 @@ schema = 3 version = "v0.1.0" hash = "sha256-Hygjq9euZ0qz6TvHYQwOZEjNiTbTh1nSLRAWZ6KFGR8=" [mod."golang.org/x/sys"] - version = "v0.6.0" - hash = "sha256-zAgxiTuL24sGhbXrna9R1UYqLQh46ldztpumOScmduY=" + version = "v0.7.0" + hash = "sha256-GotRHJaas/q3L+tFam0q3oQ1rc8GDStt7wnz9h8MTEU=" [mod."golang.org/x/term"] - version = "v0.6.0" - hash = "sha256-Ao0yXpwY8GyG+/23dVfJUYrfEfNUTES3RF45v1VhUAk=" + version = "v0.7.0" + hash = "sha256-VYnXZ50OXTsylzncIMceVC2ZBKdbyp+V367Qbq3Vlqk=" [mod."golang.org/x/text"] - version = "v0.8.0" - hash = "sha256-hgWFnT01DRmywBEXKYEVaOee7i6z8Ydz7zGbjcWwOgI=" + version = "v0.9.0" + hash = "sha256-tkhDeMsSQZr3jo7vmKehWs3DvWetwXR0IB+DCLbQ4nk=" [mod."golang.org/x/tools"] version = "v0.7.0" hash = "sha256-ZEjfFulQd6U9r4mEJ5RZOnW49NZnQnrCFLMKCgLg7go=" diff --git a/rpc/backend/call_tx.go b/rpc/backend/call_tx.go index 7967d06fe2..17d94d299d 100644 --- a/rpc/backend/call_tx.go +++ b/rpc/backend/call_tx.go @@ -384,6 +384,10 @@ func (b *Backend) DoCall( if err != nil { return nil, err } + length := len(res.Ret) + if length > int(b.cfg.JSONRPC.ReturnDataLimit) && b.cfg.JSONRPC.ReturnDataLimit != 0 { + return nil, fmt.Errorf("call retuned result on length %d exceeding limit %d", length, b.cfg.JSONRPC.ReturnDataLimit) + } if res.Failed() { if res.VmError != vm.ErrExecutionReverted.Error() { diff --git a/server/config/config.go b/server/config/config.go index 0d2b433c71..e17a85b443 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -75,6 +75,9 @@ const ( // DefaultMaxOpenConnections represents the amount of open connections (unlimited = 0) DefaultMaxOpenConnections = 0 + + // DefaultReturnDataLimit is maximum number of bytes returned from eth_call or similar invocations + DefaultReturnDataLimit = 100000 ) var evmTracers = []string{"json", "markdown", "struct", "access_list"} @@ -138,6 +141,8 @@ type JSONRPCConfig struct { MetricsAddress string `mapstructure:"metrics-address"` // FixRevertGasRefundHeight defines the upgrade height for fix of revert gas refund logic when transaction reverted FixRevertGasRefundHeight int64 `mapstructure:"fix-revert-gas-refund-height"` + // ReturnDataLimit defines maximum number of bytes returned from `eth_call` or similar invocations + ReturnDataLimit int64 `mapstructure:"return-data-limit"` } // TLSConfig defines the certificate and matching private key for the server. @@ -241,6 +246,7 @@ func DefaultJSONRPCConfig() *JSONRPCConfig { EnableIndexer: false, MetricsAddress: DefaultJSONRPCMetricsAddress, FixRevertGasRefundHeight: DefaultFixRevertGasRefundHeight, + ReturnDataLimit: DefaultReturnDataLimit, } } @@ -351,6 +357,7 @@ func GetConfig(v *viper.Viper) (Config, error) { EnableIndexer: v.GetBool("json-rpc.enable-indexer"), MetricsAddress: v.GetString("json-rpc.metrics-address"), FixRevertGasRefundHeight: v.GetInt64("json-rpc.fix-revert-gas-refund-height"), + ReturnDataLimit: v.GetInt64("json-rpc.return-data-limit"), }, TLS: TLSConfig{ CertificatePath: v.GetString("tls.certificate-path"), diff --git a/server/config/toml.go b/server/config/toml.go index a61f110dc7..0e18f45dca 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -95,6 +95,9 @@ metrics-address = "{{ .JSONRPC.MetricsAddress }}" # Upgrade height for fix of revert gas refund logic when transaction reverted. fix-revert-gas-refund-height = {{ .JSONRPC.FixRevertGasRefundHeight }} +# Maximum number of bytes returned from eth_call or similar invocations. +return-data-limit = {{ .JSONRPC.ReturnDataLimit }} + ############################################################################### ### TLS Configuration ### ############################################################################### diff --git a/server/flags/flags.go b/server/flags/flags.go index 5f3d9c7112..f0edcbdbb3 100644 --- a/server/flags/flags.go +++ b/server/flags/flags.go @@ -70,6 +70,7 @@ const ( // https://github.com/ethereum/go-ethereum/blob/master/metrics/metrics.go#L35-L55 JSONRPCEnableMetrics = "metrics" JSONRPCFixRevertGasRefundHeight = "json-rpc.fix-revert-gas-refund-height" + JSONRPCReturnDataLimit = "json-rpc.return-data-limit" ) // EVM flags diff --git a/tests/integration_tests/configs/exploit.jsonnet b/tests/integration_tests/configs/exploit.jsonnet new file mode 100644 index 0000000000..aa6bf82c6e --- /dev/null +++ b/tests/integration_tests/configs/exploit.jsonnet @@ -0,0 +1,11 @@ +local config = import 'default.jsonnet'; + +config { + 'ethermint_9000-1'+: { + 'app-config'+: { + 'json-rpc'+: { + 'return-data-limit': 3594241, // memory_byte_size + 1 + }, + }, + }, +} diff --git a/tests/integration_tests/hardhat/contracts/TestExploitContract.sol b/tests/integration_tests/hardhat/contracts/TestExploitContract.sol new file mode 100644 index 0000000000..1e2ddd44e0 --- /dev/null +++ b/tests/integration_tests/hardhat/contracts/TestExploitContract.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract TestExploitContract { + function dos() public pure { + assembly { + return(0, 0x36d800) + } + } +} \ No newline at end of file diff --git a/tests/integration_tests/test_exploit.py b/tests/integration_tests/test_exploit.py new file mode 100644 index 0000000000..ed024b3178 --- /dev/null +++ b/tests/integration_tests/test_exploit.py @@ -0,0 +1,75 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path + +import pytest +import requests +from pystarport import ports + +from .network import setup_custom_ethermint +from .utils import CONTRACTS, deploy_contract + + +@pytest.fixture(scope="module") +def custom_ethermint(tmp_path_factory): + path = tmp_path_factory.mktemp("exploit") + yield from setup_custom_ethermint( + path, 26910, Path(__file__).parent / "configs/exploit.jsonnet" + ) + + +def call(port, params): + url = f"http://127.0.0.1:{ports.evmrpc_port(port)}" + rsp = requests.post(url, json=params) + assert rsp.status_code == 200 + return rsp.json() + + +def run_test(provider, concurrent, batch, expect_cb): + _, res = deploy_contract(provider.w3, CONTRACTS["TestExploitContract"]) + param = { + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "data": "0x5e67164c", + "to": res["contractAddress"], + }, + "latest", + ], + "id": 1, + } + params = [] + for _ in range(batch): + params.append(param) + with ThreadPoolExecutor(concurrent) as executor: + tasks = [ + executor.submit(call, provider.base_port(0), params) + for _ in range(0, concurrent) + ] + results = [future.result() for future in as_completed(tasks)] + assert len(results) == concurrent + for result in results: + expect_cb(result) + + +def test_call(ethermint): + concurrent = 2 + batch = 1 + + def expect_cb(result): + for item in result: + assert "error" in item + assert "exceeding limit" in item["error"]["message"] + + run_test(ethermint, concurrent, batch, expect_cb) + + +def test_large_call(custom_ethermint): + concurrent = 2 + batch = 1 + + def expect_cb(result): + for item in result: + assert "error" not in item + + run_test(custom_ethermint, concurrent, batch, expect_cb) diff --git a/tests/integration_tests/utils.py b/tests/integration_tests/utils.py index b8920be80f..9a0c87e56a 100644 --- a/tests/integration_tests/utils.py +++ b/tests/integration_tests/utils.py @@ -32,6 +32,7 @@ "TestChainID": "ChainID.sol", "Mars": "Mars.sol", "StateContract": "StateContract.sol", + "TestExploitContract": "TestExploitContract.sol", }