Skip to content

fix(api): harden external input validation - #21

Open
0xbigapple wants to merge 7 commits into
release_v4.8.3from
fix/api-input-hardening
Open

0xbigapple wants to merge 7 commits into
release_v4.8.3from
fix/api-input-hardening

Conversation

@0xbigapple

@0xbigapple 0xbigapple commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

This PR hardens externally reachable API inputs before expensive or ambiguous processing:

  • validates contract-query addresses before database access and Base58Check formatting
  • limits string-form integer inputs before BigDecimal conversion
  • validates Permission_id exactly, rejects strings longer than 64 characters, and preserves existing supported numeric forms
  • limits shielded TRC20 amount strings before BigInteger conversion
  • validates shielded transfer spend/receive counts before serialization loops
  • rejects absent, blank, and malformed reward and brokerage addresses consistently across FullNode, Solidity, and PBFT HTTP APIs
  • returns bounded validation messages without echoing oversized attacker-controlled values

Why are these changes required?

Malformed inputs could previously trigger disproportionate CPU work, silently truncate or wrap numeric values, or return plausible results for structurally invalid addresses.

These changes reject invalid inputs before expensive conversion, encoding, storage access, or repeated buffer merging.

Compatibility notes:

  • breaking: getReward and getBrokerage no longer substitute a default for an address they never received. Util.getAddress now either returns a valid address or throws, so a request that carries no address at all is rejected like any other unusable one: GET /wallet/getReward without the parameter answered {"reward": 0} and now answers {"Error":"INVALID address"}. The same applies to getBrokerage and to both endpoints on /walletsolidity and the PBFT mirror, six in total. A caller that relied on the zero default has to send an address.
  • other valid inputs retain their existing behavior
  • Permission_id values with fractions, integer overflow, explicit null, or string representations longer than 64 characters are rejected
  • shielded TRC20 APIs remain controlled by the existing node.allowShieldedTransactionApi flag, which is disabled by default
  • no protocol, protobuf, storage, dependency, configuration, or HTTP status-code changes are introduced

This PR has been tested by:

  • Unit Tests
  • Manual Testing

Follow up

Extra details


Summary by cubic

Hardens externally reachable API input validation so malformed requests are rejected before expensive conversion, store access, or Base58 encoding, and all HTTP endpoints answer consistently. getReward/getBrokerage (and their /walletsolidity/PBFT mirrors) no longer default a missing address; they now require a valid one and return {"Error":"INVALID address"} instead of 0/20.

  • Bug Fixes

    • Util.getAddress validates Base58Check and 41... hex and returns fixed "INVALID address"/"INVALID JSON body" errors without echoing input; request bodies stay bounded by Jetty's SizeLimitHandler.
    • getContract/getContractInfo reject malformed addresses before any store access; JSON numbers are capped at 64 characters, and Permission_id must be an exact 32-bit int and match across coercions.
    • InvalidParameterException with fixed, client-safe messages is whitelisted by the error sanitizer instead of becoming "internal server error".
    • Address fields over 21 bytes render as hex with visible=true, and log, exception, and metric messages truncate oversized addresses via WalletUtil.getAddressString.
    • Shielded TRC-20: amount strings are capped at 80 characters; transfer requires 1–2 spends and 1–2 receives plus a matching spend-signature count without ASK; mint requires exactly one receive; unusable inputs surface as ContractValidateException. The node.allowShieldedTransactionApi gate is unchanged.
    • No protocol, protobuf, storage, dependency, configuration, or HTTP status-code changes.
  • Migration

    • Clients of /wallet/getReward and /wallet/getBrokerage (and their /walletsolidity/PBFT mirrors) must send a valid address; the previous zero/20 defaults are removed.

Written for commit 77572d3. Summary will update on new commits.

Review in cubic

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5dd0c1c4-9236-45b2-82b7-96978d63e49d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 15 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="framework/src/test/java/org/tron/core/services/interfaceOnSolidity/http/RewardBrokerageAddressValidationTest.java">

<violation number="1" location="framework/src/test/java/org/tron/core/services/interfaceOnSolidity/http/RewardBrokerageAddressValidationTest.java:42">
P3: The Solidity and PBFT test files duplicate the entire harness (reflection-backed wallet factory, request builder, inject helper, and the reward/brokerage rejection test) differing only in the wallet type and injected field name. Extract a shared base class or parameterized harness to avoid maintaining the same assertions in two places.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread framework/src/main/java/org/tron/core/Wallet.java Outdated
Comment thread framework/src/main/java/org/tron/core/services/http/Util.java Outdated
}

@Test
public void rewardAndBrokerageMirrorsRejectMalformedAddresses() throws Exception {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The Solidity and PBFT test files duplicate the entire harness (reflection-backed wallet factory, request builder, inject helper, and the reward/brokerage rejection test) differing only in the wallet type and injected field name. Extract a shared base class or parameterized harness to avoid maintaining the same assertions in two places.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/test/java/org/tron/core/services/interfaceOnSolidity/http/RewardBrokerageAddressValidationTest.java, line 42:

<comment>The Solidity and PBFT test files duplicate the entire harness (reflection-backed wallet factory, request builder, inject helper, and the reward/brokerage rejection test) differing only in the wallet type and injected field name. Extract a shared base class or parameterized harness to avoid maintaining the same assertions in two places.</comment>

<file context>
@@ -0,0 +1,57 @@
+  }
+
+  @Test
+  public void rewardAndBrokerageMirrorsRejectMalformedAddresses() throws Exception {
+    GetRewardOnSolidityServlet reward = new GetRewardOnSolidityServlet();
+    inject(reward, "walletOnSolidity", walletOnSolidity());
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 17 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="framework/src/test/java/org/tron/core/services/http/AddressQueryServletTestBase.java">

<violation number="1" location="framework/src/test/java/org/tron/core/services/http/AddressQueryServletTestBase.java:71">
P3: When a test address contains JSON-significant characters, `jsonRequest` creates malformed JSON instead of encoding the address as a JSON string. Escape the value with the project's JSON serializer (or otherwise quote it correctly) so this shared contract helper exercises the endpoint with the intended address.</violation>
</file>

<file name="framework/src/main/java/org/tron/core/services/http/Util.java">

<violation number="1" location="framework/src/main/java/org/tron/core/services/http/Util.java:705">
P2: For multibyte UTF-8 bodies, this check counts characters instead of request bytes and buffers well beyond `httpMaxMessageSize` before rejecting. Count bytes from the input stream while reading so the early bound matches the configured limit.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

new InputStreamReader(request.getInputStream()))) {
int read;
while ((read = reader.read(buffer)) != -1) {
if (sb.length() + read > limit) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: For multibyte UTF-8 bodies, this check counts characters instead of request bytes and buffers well beyond httpMaxMessageSize before rejecting. Count bytes from the input stream while reading so the early bound matches the configured limit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/main/java/org/tron/core/services/http/Util.java, line 705:

<comment>For multibyte UTF-8 bodies, this check counts characters instead of request bytes and buffers well beyond `httpMaxMessageSize` before rejecting. Count bytes from the input stream while reading so the early bound matches the configured limit.</comment>

<file context>
@@ -673,14 +684,33 @@ private static String checkGetParam(HttpServletRequest request, String key) thro
+        new InputStreamReader(request.getInputStream()))) {
+      int read;
+      while ((read = reader.read(buffer)) != -1) {
+        if (sb.length() + read > limit) {
+          throw new Exception("body size is too big, the limit is " + limit);
+        }
</file context>


protected static MockHttpServletRequest jsonRequest(String address, String contentType) {
MockHttpServletRequest request = postRequest(contentType);
String json = address == null ? "{}" : "{\"address\":\"" + address + "\"}";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When a test address contains JSON-significant characters, jsonRequest creates malformed JSON instead of encoding the address as a JSON string. Escape the value with the project's JSON serializer (or otherwise quote it correctly) so this shared contract helper exercises the endpoint with the intended address.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/test/java/org/tron/core/services/http/AddressQueryServletTestBase.java, line 71:

<comment>When a test address contains JSON-significant characters, `jsonRequest` creates malformed JSON instead of encoding the address as a JSON string. Escape the value with the project's JSON serializer (or otherwise quote it correctly) so this shared contract helper exercises the endpoint with the intended address.</comment>

<file context>
@@ -0,0 +1,196 @@
+
+  protected static MockHttpServletRequest jsonRequest(String address, String contentType) {
+    MockHttpServletRequest request = postRequest(contentType);
+    String json = address == null ? "{}" : "{\"address\":\"" + address + "\"}";
+    request.setContent(json.getBytes(UTF_8));
+    return request;
</file context>

@0xbigapple
0xbigapple force-pushed the fix/api-input-hardening branch 4 times, most recently from 2c3bb5c to 248cbe0 Compare August 25, 2026 13:56

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 40 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="framework/src/main/java/org/tron/core/services/http/JsonFormat.java">

<violation number="1" location="framework/src/main/java/org/tron/core/services/http/JsonFormat.java:864">
P3: `escapeBytesSelfType` hex-renders the entire oversized ByteString (2 hex chars per byte), so a 64KB attacker-controlled address field is echoed as a ~128KB hex string in visible-mode responses — larger than the base58 rendering it replaces and contrary to the PR's stated goal of avoiding echoing oversized attacker-controlled values. The log-side helper introduced in the same change truncates to a 4-byte head; this response path does not.</violation>

<violation number="2" location="framework/src/main/java/org/tron/core/services/http/JsonFormat.java:1708">
P2: This guard still accepts non-empty 1–20-byte hex values as addresses, while the visible Base58 path rejects any length other than 21 bytes. Reject non-empty lengths other than `Constant.TRON_ADDRESS_SIZE` here, preserving empty optional address fields.</violation>
</file>

<file name="framework/src/test/java/org/tron/core/db/Base58ValidationErrorTest.java">

<violation number="1" location="framework/src/test/java/org/tron/core/db/Base58ValidationErrorTest.java:47">
P3: Both tests assert only the `,len=16384)` tail of the invalid-address render, so they pass even if the new `invalid-address(<hex-head>…)` prefix is lost. Pin the full prefix to lock in the PR's visible-hex rendering contract.

Replace the contains check in both tests with an assertion on the full prefix, e.g. `contains("invalid-address(00000000…,len=16384)")`.</violation>
</file>

<file name="framework/src/test/java/org/tron/core/WalletMockTest.java">

<violation number="1" location="framework/src/test/java/org/tron/core/WalletMockTest.java:910">
P3: This transfer test passes with a completely empty ReceiveDescription (zero-length commitment, value, epk, zkproof, c_enc, c_out), so it only proves the new count check (1-2 receives) succeeds. The PR claims unusable inputs are rejected with ContractValidateException, but a fully empty receive description still produces a trigger input here. Either use a populated ReceiveDescription or add assertions that empty/invalid receive descriptions are rejected, so the hardened validation path is actually exercised.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

if (!selfType) {
result = unescapeBytes(escaped);
if (HttpSelfFormatFieldName.isAddressFormat(fieldName)
&& result.size() > Constant.TRON_ADDRESS_SIZE) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This guard still accepts non-empty 1–20-byte hex values as addresses, while the visible Base58 path rejects any length other than 21 bytes. Reject non-empty lengths other than Constant.TRON_ADDRESS_SIZE here, preserving empty optional address fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/main/java/org/tron/core/services/http/JsonFormat.java, line 1708:

<comment>This guard still accepts non-empty 1–20-byte hex values as addresses, while the visible Base58 path rejects any length other than 21 bytes. Reject non-empty lengths other than `Constant.TRON_ADDRESS_SIZE` here, preserving empty optional address fields.</comment>

<file context>
@@ -1701,6 +1704,10 @@ public ByteString consumeByteString(final String fieldName, boolean selfType)
         if (!selfType) {
           result = unescapeBytes(escaped);
+          if (HttpSelfFormatFieldName.isAddressFormat(fieldName)
+              && result.size() > Constant.TRON_ADDRESS_SIZE) {
+            throw new InvalidEscapeSequence("invalid address for field: " + fieldName);
+          }
</file context>
Suggested change
&& result.size() > Constant.TRON_ADDRESS_SIZE) {
&& result.size() != 0
&& result.size() != Constant.TRON_ADDRESS_SIZE) {

ContractValidateException failure = assertThrows(ContractValidateException.class,
trace::checkIsConstant);
assertTrue(failure.getMessage(),
failure.getMessage().contains(",len=16384)"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Both tests assert only the ,len=16384) tail of the invalid-address render, so they pass even if the new invalid-address(<hex-head>…) prefix is lost. Pin the full prefix to lock in the PR's visible-hex rendering contract.

Replace the contains check in both tests with an assertion on the full prefix, e.g. contains("invalid-address(00000000…,len=16384)").

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/test/java/org/tron/core/db/Base58ValidationErrorTest.java, line 47:

<comment>Both tests assert only the `,len=16384)` tail of the invalid-address render, so they pass even if the new `invalid-address(<hex-head>…)` prefix is lost. Pin the full prefix to lock in the PR's visible-hex rendering contract.

Replace the contains check in both tests with an assertion on the full prefix, e.g. `contains("invalid-address(00000000…,len=16384)")`.</comment>

<file context>
@@ -0,0 +1,74 @@
+      ContractValidateException failure = assertThrows(ContractValidateException.class,
+          trace::checkIsConstant);
+      assertTrue(failure.getMessage(),
+          failure.getMessage().contains(",len=16384)"));
+      when(properties.getAllowTvmConstantinople()).thenReturn(1L);
+      trace.checkIsConstant();
</file context>

GrpcAPI.ShieldedTRC20Parameters shieldedTRC20Parameters =
GrpcAPI.ShieldedTRC20Parameters.newBuilder()
.addSpendDescription(spendDescription)
.addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This transfer test passes with a completely empty ReceiveDescription (zero-length commitment, value, epk, zkproof, c_enc, c_out), so it only proves the new count check (1-2 receives) succeeds. The PR claims unusable inputs are rejected with ContractValidateException, but a fully empty receive description still produces a trigger input here. Either use a populated ReceiveDescription or add assertions that empty/invalid receive descriptions are rejected, so the hardened validation path is actually exercised.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/test/java/org/tron/core/WalletMockTest.java, line 910:

<comment>This transfer test passes with a completely empty ReceiveDescription (zero-length commitment, value, epk, zkproof, c_enc, c_out), so it only proves the new count check (1-2 receives) succeeds. The PR claims unusable inputs are rejected with ContractValidateException, but a fully empty receive description still produces a trigger input here. Either use a populated ReceiveDescription or add assertions that empty/invalid receive descriptions are rejected, so the hardened validation path is actually exercised.</comment>

<file context>
@@ -907,6 +907,7 @@ public void testGetTriggerInputForShieldedTRC20Contract1()
     GrpcAPI.ShieldedTRC20Parameters shieldedTRC20Parameters =
         GrpcAPI.ShieldedTRC20Parameters.newBuilder()
             .addSpendDescription(spendDescription)
+            .addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance())
             .setParameterType("transfer")
             .build();
</file context>

static String escapeBytesSelfType(ByteString input, final String fieldName) {
//Address
if (HttpSelfFormatFieldName.isAddressFormat(fieldName)) {
if (input.size() > Constant.TRON_ADDRESS_SIZE) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: escapeBytesSelfType hex-renders the entire oversized ByteString (2 hex chars per byte), so a 64KB attacker-controlled address field is echoed as a ~128KB hex string in visible-mode responses — larger than the base58 rendering it replaces and contrary to the PR's stated goal of avoiding echoing oversized attacker-controlled values. The log-side helper introduced in the same change truncates to a 4-byte head; this response path does not.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At framework/src/main/java/org/tron/core/services/http/JsonFormat.java, line 864:

<comment>`escapeBytesSelfType` hex-renders the entire oversized ByteString (2 hex chars per byte), so a 64KB attacker-controlled address field is echoed as a ~128KB hex string in visible-mode responses — larger than the base58 rendering it replaces and contrary to the PR's stated goal of avoiding echoing oversized attacker-controlled values. The log-side helper introduced in the same change truncates to a 4-byte head; this response path does not.</comment>

<file context>
@@ -861,6 +861,9 @@ static String escapeBytes(ByteString input, final String fieldName, boolean self
   static String escapeBytesSelfType(ByteString input, final String fieldName) {
     //Address
     if (HttpSelfFormatFieldName.isAddressFormat(fieldName)) {
+      if (input.size() > Constant.TRON_ADDRESS_SIZE) {
+        return ByteArray.toHexString(input.toByteArray());
+      }
</file context>

getContract and getContractInfo answer null for an address that is not well
formed, before the account store is read.
A string or number longer than 64 characters is rejected before BigDecimal
sees it, and Permission_id must convert identically through both coercion
paths, so a value one path silently reinterprets no longer gets through.
@0xbigapple
0xbigapple changed the base branch from develop to release_v4.8.3 September 29, 2026 09:03
@0xbigapple
0xbigapple force-pushed the fix/api-input-hardening branch from a6de2cc to caa932e Compare September 29, 2026 09:04
getAddress reports every way a request can fail to name an address as one
fixed message, and the reward and brokerage servlets write it, so the
fullnode, solidity and PBFT endpoints no longer differ on a given malformed
request. An absent address is one of those ways: it used to be answered with
the service default.
Mint takes exactly one receive description; transfer takes one or two of
each and, without an ask, a matching spend authority signature count. The
counts are checked before the merge loops, because ByteUtil.merge copies the
accumulated buffer on every iteration and checking afterwards would make an
unbounded list quadratic.
… failure

An amount string past 80 characters and a trigger input the builder rejects
are answered as a ContractValidateException naming the argument, instead of
escaping the wallet as an IllegalArgumentException.
JsonFormat renders address fields over 21 bytes as hex instead of Base58
(visible=true). The hello handshake, PBFT SR list, trigger constant check and
getContract logs render addresses via WalletUtil.getAddressString, truncating
oversized input instead of full Base58Check encoding.
The response sanitizer added in the release only forwards a whitelist of
exception messages and turns the rest into "internal server error". The
bounded json-number validation raises InvalidParameterException with a
client-safe, fixed message (e.g. the id length limit), so add it to the
whitelist; other InvalidParameterException messages in the http layer are
fixed texts too.
@0xbigapple
0xbigapple force-pushed the fix/api-input-hardening branch from caa932e to 77572d3 Compare September 29, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant