fix(jsonrpc): harden RPC/HTTP parameter validation#6828
Conversation
Add length/format guards across the JSON-RPC and HTTP address-handling paths so
malformed or oversized parameters are rejected early with a clear -32602 / error
response, instead of triggering O(n^2) work or leaking NPE / Internal errors.
- Commons.decodeFromBase58Check: reject non-34-char input before the O(n^2)
Base58 decode; single funnel covering every HTTP address input.
- JsonFormat.unescapeBytesSelfType: turn an invalid Base58 address (null or
illegal-char) into a clear "invalid address" error instead of a bare
ByteString.copyFrom(null) NPE; covers all merge-path address fields.
- JsonRpcApiUtil.addressCompatibleToByteArray: cap length at ADDRESS_SIZE + 2
(the +2 keeps 0x-prefixed 21-byte addresses valid) before fromHexString.
- JsonRpcApiUtil.parseBlockNumber: tighten MAX_BLOCK_NUM_HEX_LEN 100 -> 20 and
route the (String, Wallet) overload through the single-arg parser so it also
gets the length cap, overflow (longValueExact) and negative checks.
- TronJsonRpcImpl.getStorageAt: bound the storage key and parse it before the
contract lookup; wrap DataWord parsing so a >32-byte key
returns invalid-params instead of an Internal error.
- Hoist hashToByteArray/HASH_REGEX into JsonRpcApiUtil so LogFilterWrapper's
blockHash gets the same 32-byte-hash validation.
…and address
Continue hardening the eth_* JSON-RPC parameter paths so malformed, oversized
or overflowing inputs are rejected with a clear -32602 (or, for an out-of-range
index, a null result) instead of leaking an Internal error, an uncaught
Error/NPE, a DecoderException, or a silently wrong value.
- eth_getTransactionByBlock{Hash,Number}AndIndex: parse the index via a new
length-capped JsonRpcApiUtil.parseTxIndex (malformed / oversized / int-overflow
index -> -32602); in the handler, a negative or out-of-range index
(txIndex < 0 || >= tx count) returns a null result instead of letting
block.getTransactions(-1) throw IndexOutOfBoundsException (-32603).
- buildCreateSmartContractTransaction: a deeply nested ABI overflows the
recursive JsonFormat parser; catch the StackOverflowError narrowly around the
merge and map it to invalid-params ("invalid abi") instead of letting an
Error escape.
- buildCreate/buildTriggerSmartContractTransaction: compute feeLimit via
JsonRpcApiUtil.calcFeeLimit (Math.multiplyExact) so an oversized gas no
longer silently wraps gas*energyFee to a bogus (possibly negative) feeLimit.
- parseQuantityValue: reject a signed ("0x-..") value/gas; QUANTITY is unsigned.
- LogFilter topics: parse via the strict hashToByteArray (regex-validated 32-byte
hash) instead of topicToByteArray, and remove the now-unused topicToByteArray.
- addressToByteArray: cap length before fromHexString and wrap the decode so an
oversized or invalid-hex address filter is invalid-params.
…er elements
- Restore topicToByteArray for LogFilter topics, guard with (0x)?[0-9a-fA-F]{63,64}$ so the stripped zero is padded back by ByteArray.fromHexString, while non-hex or wrong-length input still gets a clean -32602.
- LogFilter: validate element types before the (String) cast in the address array and nested topic array loops.
| * check if topic is hex string of size 64, padding 0 ahead if length is odd. | ||
| * Matches a 32-byte topic hex string: optional 0x prefix + 63 or 64 hex chars. | ||
| */ | ||
| public static final String TOPIC_REGEX = "(0x)?[0-9a-fA-F]{63,64}$"; |
There was a problem hiding this comment.
[SHOULD] PR description item 12 contradicts the final implementation
The description says topics are now parsed "via the strict hashToByteArray ... instead of topicToByteArray (now removed)" and that odd-length input is rejected ("no silent leading-0 pad"). The final code does the opposite: topicToByteArray is kept, TOPIC_REGEX accepts 63 or 64 hex chars, and a 63-char topic is accepted and zero-padded (locked in by topicToByteArrayPadsMissingLeadingZero). Anyone auditing the behavior change from the description will reach the wrong conclusion about what eth_getLogs / eth_newFilter accept.
Suggestion: Update description item 12 to match the code: topics still go through topicToByteArray, which now enforces a 63/64-char length cap and strict hex, while keeping the odd-length leading-zero compatibility.
There was a problem hiding this comment.
Updated the PR description.
What does this PR do?
Adds length/format validation to several JSON-RPC and HTTP parameter paths, so malformed,
oversized, or overflowing input is rejected early — a clear
-32602(or anullresult for anout-of-range index) — instead of triggering
O(n²)work, leaking aNullPointerException/DecoderException/ Internal error, or silently producing a wrong value. Valid input is unaffected.Commons.decodeFromBase58Check— reject input whose length!= 34before theO(n²)Base58.decode; single funnel for every HTTP address input.JsonFormat.unescapeBytesSelfType— an invalid Base58 address (null / illegal char) now returns a clearinvalid addresserror instead of a bareByteString.copyFrom(null)NPE; covers all merge-path address fields.JsonRpcApiUtil.addressCompatibleToByteArray— cap length atADDRESS_SIZE + 2(the+2keeps0x-prefixed 21-byte addresses valid) beforefromHexString.JsonRpcApiUtil.parseBlockNumber— tightenMAX_BLOCK_NUM_HEX_LEN100 → 20, and route the(String, Wallet)overload through the single-arg parser so it also gets the length cap checks.TronJsonRpcImpl.getStorageAt— bound the storage key and parse it before the contract lookup; a>32-byte key now returns-32602 invalid storage key valueinstead of-32001 (json4j default error).LogFilterWrapper— validateblockHashvia the sharedJsonRpcApiUtil.hashToByteArray(moved here fromTronJsonRpcImpl).HASH_REGEXaccepts only hex digits[0-9a-fA-F], so a non-hex hash is rejected asinvalid hash valuerather than slipping through tofromHexString.JsonRpcApiUtil.parseTxIndex— length-capped 0x-hex parse for the tx index; a malformed / oversized / int-overflow index returns-32602, and a negative or out-of-range index returns anullresult instead of lettingblock.getTransactions(-1)throwIndexOutOfBoundsException(Internal error).TronJsonRpcImpl.buildCreateSmartContractTransaction— a deeply nested ABI overflows the recursiveJsonFormatparser; theStackOverflowErroris caught narrowly around the merge and mapped to-32602 invalid abiinstead of anErrorescaping as an Internal error.JsonRpcApiUtil.calcFeeLimit— computefeeLimit = gas * energyFeeviaStrictMathWrapper.multiplyExact, so an oversizedgasreturns-32602instead of silently wrapping to a bogus (possibly negative)feeLimit.JsonRpcApiUtil.parseQuantityValue— reject a signed (0x-..)value/gas;QUANTITYis unsigned, so a negative parse is-32602rather than a negativecallValue/feeLimitreaching the actuator.LogFiltertopics —topicToByteArraynow validates topics viaTOPIC_REGEX(^(0x)?[0-9a-fA-F]{63,64}$); an invalid-hex / wrong-length / non-string topic returns-32602instead of a leakedDecoderException/ClassCastException, while keeping the odd-length (63-char) leading-0compatibility.JsonRpcApiUtil.addressToByteArray(log-filter address) — cap length beforefromHexStringand wrap the decode, so an oversized or invalid-hex address filter returns-32602instead ofO(n)work or a leakedDecoderException;a nested non-string address element (e.g.{"address":[1]}) is likewise rejected as-32602before the(String)cast; the 20-byte output and strict (no41prefix) semantics are unchanged.getBlockReceipts+parseBlockNumber(String, Wallet)— anullblock param dereferenced before validation (Pattern.matches(HASH_REGEX, null)/null.startsWith("0x")) NPE'd as an Internal error; it now returns-32602.Why are these changes required?
Several unauthenticated API entry points accepted unbounded / malformed input, causing two classes of problem:
decodeFromBase58Checkfed the whole string into theO(n²)Base58.decode; since the cost grows quadratically with length, a single oversized addresspayload (well within the 4 MB body cap) can tie up a request thread far out of proportion to
its size — a cheap DoS the body cap and rate limiter don't stop. Block-number parsing likewise
had no effective length bound before the
O(n²)BigIntegerconstructor.bare
NullPointerException;eth_getStorageAtsurfaced aRuntimeExceptionas-32001 Data word can't exceed 32 bytes: xxx; a negative tx index hitgetTransactions(-1)(
IndexOutOfBoundsException); a malformed log topic / address leaked aDecoderException; adeeply nested ABI exhausted the recursive
JsonFormatparser's stack (StackOverflowError); anullblock param NPE'd onPattern.matches/startsWith— all escaping as Internal errorsinstead of
-32602 invalid params.This PR has been tested by:
Follow up
Extra details
Representative affected endpoints, by guarded parameter (each parameter is a single guarded
funnel, so the lists below are examples, not exhaustive):
eth_getBlockByNumber,getBlockReceipts, …hashToByteArray):eth_getBlockByHash,eth_getTransactionByBlockHashAndIndex, …hashToByteArray):eth_getTransactionByHash,eth_getTransactionReceipteth_getStorageAtADDRESS_SIZE + 2):eth_call,eth_getBalance…null):eth_getTransactionByBlockHashAndIndex,eth_getTransactionByBlockNumberAndIndexhashToByteArray/ length-cappedaddressToByteArray):eth_getLogs,eth_newFilterbuildTransactionnull→-32602):eth_getBlockReceipts/wallet/*— all covered by the Base58 length/DoS guard, but they differ on an invalid address:Util.getHexAddressand just return{}(e.g.getcontract) — unchanged;getaccount,visible=true) get the NPE →invalid addressfix;Util.getAddress(request)(e.g.getReward) get null for an invalid address and degrade toreward 0.