From 24331986ac05da998770d40c7466bbba4433b412 Mon Sep 17 00:00:00 2001 From: waynercheung Date: Fri, 28 Aug 2026 18:21:22 +0800 Subject: [PATCH] fix(http): bound JsonFormat integer and enum token parsing Reject integer tokens longer than 256 characters as the first operation in parseInteger, before substring creation and BigInteger parsing. Pass the original token to BigInteger without normalization and retain the existing exact range checks. Reject enum identifier tokens longer than 256 characters, quotes included, before case normalization and descriptor lookup. Abbreviate identifiers echoed in enum errors to 64 characters, without splitting a UTF-16 surrogate pair. Use fixed messages for integer range errors and unsigned negative values, without including the input token. Bound the JDK detail retained for integer syntax errors. Replace float and double parse error details with a message containing only the token length. Leave string and bytes fields, and ignored unknown field names, outside these limits. Add parser and servlet tests covering integer token limits, both integer parser branches, exact signed int64 boundaries, enum identifiers, bounded error messages, and existing string, bytes and unknown-field handling. BREAKING CHANGE: integer tokens longer than 256 characters are now rejected, including zero-padded values that previously parsed successfully. Enum identifier tokens are limited to 256 characters. Integer range, unsigned-negative, enum lookup, and floating-point parse error messages have changed. --- .../tron/core/services/http/JsonFormat.java | 101 ++++++-- .../http/DeployContractServletTest.java | 54 ++++ .../http/JsonFormatErrorBoundaryTest.java | 83 ++++++ .../http/JsonFormatIntegerTokenTest.java | 237 ++++++++++++++++++ .../core/services/http/JsonFormatTest.java | 29 +++ .../services/http/JsonFormatTestSupport.java | 32 +++ 6 files changed, 511 insertions(+), 25 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/JsonFormatErrorBoundaryTest.java create mode 100644 framework/src/test/java/org/tron/core/services/http/JsonFormatIntegerTokenTest.java create mode 100644 framework/src/test/java/org/tron/core/services/http/JsonFormatTestSupport.java diff --git a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java index 2fa7d9fbb42..06686fda5b8 100644 --- a/framework/src/main/java/org/tron/core/services/http/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/JsonFormat.java @@ -82,6 +82,21 @@ public class JsonFormat { private static final String EXPECTED_STRING = "Expected string."; private static final String MISSING_END_QUOTE = "String missing ending quote."; + // Bounds all work and error messages performed by parseInteger after tokenization. + static final int MAX_INTEGER_TOKEN_LENGTH = 256; + + // Enum identifiers are schema-defined and small. This is an input-contract and performance + // limit, not the error-response bound; abbreviateIdentifier() bounds errors independently. + // Keep it separate from generic identifiers, strings and bytes so large values remain valid. + static final int MAX_ENUM_TOKEN_LENGTH = 256; + + private static final int MAX_IDENTIFIER_ECHO_PREFIX_LENGTH = 64; + + // Caps only the JDK NumberFormatException detail retained for integer syntax errors. The detail + // may contain an input prefix. Supported JDKs emit shorter messages in tested cases, but their + // message format is not a stable contract, so keep this limit as a defensive boundary. + private static final int MAX_INTEGER_ERROR_DETAIL_PREFIX_LENGTH = 256; + public static final boolean ALWAYS_OUTPUT_DEFAULT_VALUE_FIELDS = true; public static final Set> MESSAGES = ImmutableSet.of( BalanceContract.AccountBalanceResponse.class, @@ -766,6 +781,13 @@ private static Object handlePrimitive(Tokenizer tokenizer, FieldDescriptor field + number + "."); } } else { + String enumToken = tokenizer.currentToken(); + // Reject before case normalization and descriptor lookup. Error echo is bounded + // independently by abbreviateIdentifier(). + if (enumToken.length() > MAX_ENUM_TOKEN_LENGTH) { + throw tokenizer.parseException("Enum token is too long: length " + + enumToken.length() + ", max " + MAX_ENUM_TOKEN_LENGTH); + } String id = tokenizer.consumeIdentifier(); if (StringUtils.isAllLowerCase(id)) { char b = id.charAt(0); @@ -778,7 +800,7 @@ private static Object handlePrimitive(Tokenizer tokenizer, FieldDescriptor field throw tokenizer.parseExceptionPreviousToken("Enum type \"" + enumType.getFullName() + "\" has no value named \"" - + id + "\"."); + + abbreviateIdentifier(id) + "\"."); } } @@ -1126,14 +1148,50 @@ static long parseUInt64(String text) throws NumberFormatException { return parseInteger(text, false, true); } + private static String truncate(String text, int maxPrefixLength) { + if (text == null) { + return "null"; + } + if (text.length() <= maxPrefixLength) { + return text; + } + int prefixEnd = maxPrefixLength; + if (prefixEnd > 0 + && Character.isHighSurrogate(text.charAt(prefixEnd - 1)) + && Character.isLowSurrogate(text.charAt(prefixEnd))) { + --prefixEnd; + } + return text.substring(0, prefixEnd) + "...(truncated)"; + } + + private static String integerOutOfRangeMessage(boolean isSigned, boolean isLong) { + return "Number out of range for " + (isLong ? "64" : "32") + "-bit " + + (isSigned ? "signed" : "unsigned") + " integer."; + } + + private static String abbreviateIdentifier(String text) { + return truncate(text, MAX_IDENTIFIER_ECHO_PREFIX_LENGTH); + } + + private static String abbreviateIntegerError(String text) { + return truncate(text, MAX_INTEGER_ERROR_DETAIL_PREFIX_LENGTH); + } + private static long parseInteger(String text, boolean isSigned, boolean isLong) throws NumberFormatException { + // This must remain the first operation: no substring, large error message or BigInteger + // construction may happen before the raw token is bounded. + if (text.length() > MAX_INTEGER_TOKEN_LENGTH) { + throw new NumberFormatException("Integer token is too long: length " + text.length() + + ", max " + MAX_INTEGER_TOKEN_LENGTH); + } + int pos = 0; boolean negative = false; if (text.startsWith("-", pos)) { if (!isSigned) { - throw new NumberFormatException("Number must be positive: " + text); + throw new NumberFormatException("Number must be positive."); } ++pos; negative = true; @@ -1163,17 +1221,17 @@ private static long parseInteger(String text, boolean isSigned, boolean isLong) if (!isLong) { if (isSigned) { if ((result > Integer.MAX_VALUE) || (result < Integer.MIN_VALUE)) { - throw new NumberFormatException("Number out of range for 32-bit signed integer: " - + text); + throw new NumberFormatException(integerOutOfRangeMessage(isSigned, isLong)); } } else { if ((result >= (1L << 32)) || (result < 0)) { - throw new NumberFormatException("Number out of range for 32-bit unsigned integer: " - + text); + throw new NumberFormatException(integerOutOfRangeMessage(isSigned, isLong)); } } } } else { + // Preserve existing syntax behavior by passing numberText to BigInteger without additional + // normalization. BigInteger bigValue = new BigInteger(numberText, radix); if (negative) { bigValue = bigValue.negate(); @@ -1183,25 +1241,21 @@ private static long parseInteger(String text, boolean isSigned, boolean isLong) if (!isLong) { if (isSigned) { if (bigValue.bitLength() > 31) { - throw new NumberFormatException("Number out of range for 32-bit signed integer: " - + text); + throw new NumberFormatException(integerOutOfRangeMessage(isSigned, isLong)); } } else { if (bigValue.bitLength() > 32) { - throw new NumberFormatException("Number out of range for 32-bit unsigned integer: " - + text); + throw new NumberFormatException(integerOutOfRangeMessage(isSigned, isLong)); } } } else { if (isSigned) { if (bigValue.bitLength() > 63) { - throw new NumberFormatException("Number out of range for 64-bit signed integer: " - + text); + throw new NumberFormatException(integerOutOfRangeMessage(isSigned, isLong)); } } else { if (bigValue.bitLength() > 64) { - throw new NumberFormatException("Number out of range for 64-bit unsigned integer: " - + text); + throw new NumberFormatException(integerOutOfRangeMessage(isSigned, isLong)); } } } @@ -1592,8 +1646,8 @@ public double consumeDouble() throws ParseException { double result = Double.parseDouble(currentToken); nextToken(); return result; - } catch (NumberFormatException e) { - throw floatParseException(e); + } catch (NumberFormatException ignored) { + throw floatParseException(); } } @@ -1617,8 +1671,8 @@ public float consumeFloat() throws ParseException { float result = Float.parseFloat(currentToken); nextToken(); return result; - } catch (NumberFormatException e) { - throw floatParseException(e); + } catch (NumberFormatException ignored) { + throw floatParseException(); } } @@ -1737,15 +1791,12 @@ public ParseException parseExceptionPreviousToken(String description) { * when trying to parse an integer. */ private ParseException integerParseException(NumberFormatException e) { - return parseException("Couldn't parse integer: " + e.getMessage()); + return parseException("Couldn't parse integer: " + abbreviateIntegerError(e.getMessage())); } - /** - * Constructs an appropriate {@link ParseException} for the given {@code NumberFormatException} - * when trying to parse a float or double. - */ - private ParseException floatParseException(NumberFormatException e) { - return parseException("Couldn't parse number: " + e.getMessage()); + /** Constructs a bounded {@link ParseException} without forwarding the JDK parse error text. */ + private ParseException floatParseException() { + return parseException("Couldn't parse number: token length " + currentToken.length()); } } diff --git a/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java index 703f278c890..72992e02d34 100644 --- a/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/DeployContractServletTest.java @@ -1,16 +1,22 @@ package org.tron.core.services.http; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import com.google.common.base.Strings; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.tron.core.capsule.TransactionCapsule; +import org.tron.json.JSONObject; import org.tron.protos.Protocol; import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; @@ -82,4 +88,52 @@ public void testDeployContractOmitsNullAbiOutputs() throws Exception { eq(Protocol.Transaction.Contract.ContractType.CreateSmartContract)); assertTransactionResponse(response); } + + @Test + public void testRejectsOversizedAbiIntegerWithBoundedResponse() throws Exception { + // This test scopes the response bound to the ABI parser used by this endpoint. Numeric + // request fields use a separate conversion path and are outside this test's scope. + String oversizedValue = Strings.repeat("9", 100_000); + String jsonParam = "{" + + "\"owner_address\":\"4199357684BC659F5166046B56C95A0E99F1265CD1\"," + + "\"abi\":\"[{\\\"name\\\":\\\"x\\\",\\\"type\\\":" + + oversizedValue + "}]\"" + + "}"; + MockHttpServletResponse response = newResponse(); + + servlet.doPost(postRequest(jsonParam), response); + + String content = response.getContentAsString(); + JSONObject result = JSONObject.parseObject(content); + String error = result.getString("Error"); + assertNotNull("Missing Error field in response: " + content, error); + assertTrue(error.contains("Integer token is too long")); + assertTrue("Unexpectedly large response", response.getContentAsByteArray().length < 512); + assertFalse("Response echoes the request token", content.contains(Strings.repeat("9", 8))); + verifyNoInteractions(wallet); + } + + @Test + public void testRejectsOversizedAbiEnumIdentifierWithBoundedResponse() throws Exception { + // This test scopes the response bound to the ABI parser used by this endpoint. Numeric + // request fields use a separate conversion path and are outside this test's scope. + String oversizedValue = Strings.repeat("9", 100_000); + String jsonParam = "{" + + "\"owner_address\":\"4199357684BC659F5166046B56C95A0E99F1265CD1\"," + + "\"abi\":\"[{\\\"name\\\":\\\"x\\\",\\\"type\\\":\\\"" + + oversizedValue + "\\\"}]\"" + + "}"; + MockHttpServletResponse response = newResponse(); + + servlet.doPost(postRequest(jsonParam), response); + + String content = response.getContentAsString(); + JSONObject result = JSONObject.parseObject(content); + String error = result.getString("Error"); + assertNotNull("Missing Error field in response: " + content, error); + assertTrue(error.contains("Enum token is too long")); + assertTrue("Unexpectedly large response", response.getContentAsByteArray().length < 512); + assertFalse("Response echoes the request token", content.contains(Strings.repeat("9", 8))); + verifyNoInteractions(wallet); + } } diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatErrorBoundaryTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatErrorBoundaryTest.java new file mode 100644 index 00000000000..f1707ccdeb0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatErrorBoundaryTest.java @@ -0,0 +1,83 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.tron.core.services.http.JsonFormatTestSupport.assertBoundedWithoutRepeatedInput; +import static org.tron.core.services.http.JsonFormatTestSupport.mergeAbi; + +import com.google.common.base.Strings; +import org.junit.Test; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry; +import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; + +public class JsonFormatErrorBoundaryTest { + + private static final int LARGE_TOKEN_LENGTH = 65_536; + + @Test + public void oversizedEnumIdentifierIsRejectedWithoutEcho() { + String value = Strings.repeat("9", 100_000); + + JsonFormat.ParseException error = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"name\":\"x\",\"type\":\"" + value + "\"}]")); + + assertTrue(error.getMessage().contains("Enum token is too long")); + assertBoundedWithoutRepeatedInput(error.getMessage(), '9'); + } + + @Test + public void enumRawLimitIncludesQuotes() { + String atLimit = Strings.repeat("A", JsonFormat.MAX_ENUM_TOKEN_LENGTH - 2); + String overLimit = Strings.repeat("A", JsonFormat.MAX_ENUM_TOKEN_LENGTH - 1); + + JsonFormat.ParseException atLimitError = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"name\":\"x\",\"type\":\"" + atLimit + "\"}]")); + JsonFormat.ParseException overLimitError = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"name\":\"x\",\"type\":\"" + overLimit + "\"}]")); + + // Values at the enum-token limit retain a bounded identifier prefix for diagnostics. + // Oversized enum tokens take the fixed no-echo rejection path. + String expectedPrefix = Strings.repeat("A", 64) + "...(truncated)"; + assertFalse(atLimitError.getMessage().contains("Enum token is too long")); + assertFalse(atLimitError.getMessage().contains(atLimit)); + assertTrue(atLimitError.getMessage().contains(expectedPrefix)); + assertTrue("Unexpectedly long error message", atLimitError.getMessage().length() < 512); + assertTrue(overLimitError.getMessage().contains("Enum token is too long")); + assertBoundedWithoutRepeatedInput(overLimitError.getMessage(), 'A'); + } + + @Test + public void oversizedUnknownFieldNameRemainsIgnored() throws Exception { + String unknownName = Strings.repeat("a", 100_000); + + ABI abi = mergeAbi("[{\"name\":\"x\",\"" + unknownName + "\":1}]"); + + assertEquals(1, abi.getEntrysCount()); + assertEquals("x", abi.getEntrys(0).getName()); + } + + @Test + public void largeStringTokenRemainsAccepted() throws Exception { + String value = Strings.repeat("A", LARGE_TOKEN_LENGTH); + Entry.Builder builder = Entry.newBuilder(); + + JsonFormat.merge("{\"name\":\"" + value + "\"}", builder, false); + + assertEquals(value, builder.getName()); + } + + @Test + public void largeBytesTokenRemainsAccepted() throws Exception { + String value = Strings.repeat("ab", LARGE_TOKEN_LENGTH / 2); + TriggerSmartContract.Builder builder = TriggerSmartContract.newBuilder(); + + JsonFormat.merge("{\"data\":\"" + value + "\"}", builder, false); + + assertEquals(LARGE_TOKEN_LENGTH / 2, builder.getData().size()); + assertEquals((byte) 0xab, builder.getData().byteAt(0)); + assertEquals((byte) 0xab, builder.getData().byteAt(builder.getData().size() - 1)); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatIntegerTokenTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatIntegerTokenTest.java new file mode 100644 index 00000000000..a774efbae39 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatIntegerTokenTest.java @@ -0,0 +1,237 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.tron.core.services.http.JsonFormatTestSupport.assertBoundedWithoutRepeatedInput; +import static org.tron.core.services.http.JsonFormatTestSupport.mergeAbi; +import static org.tron.core.services.http.JsonFormatTestSupport.repeat; + +import org.junit.Test; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI.Entry.StateMutabilityType; + +public class JsonFormatIntegerTokenTest { + + private static void assertOutOfRangeWithoutRun(NumberFormatException error, + String expectedMessage, char input) { + assertEquals(expectedMessage, error.getMessage()); + assertBoundedWithoutRepeatedInput(error.getMessage(), input); + } + + private static void assertWrappedOutOfRangeWithoutRun(JsonFormat.ParseException error, + String expectedMessage, char input) { + assertTrue("Missing semantic range error", + error.getMessage().contains(expectedMessage)); + assertBoundedWithoutRepeatedInput(error.getMessage(), input); + } + + @Test + public void validAbiStillParses() throws Exception { + ABI abi = mergeAbi("[{\"name\":\"x\",\"type\":\"Function\"," + + "\"stateMutability\":\"Payable\"}]"); + + assertEquals(1, abi.getEntrysCount()); + Entry entry = abi.getEntrys(0); + assertEquals(Entry.EntryType.Function, entry.getType()); + assertEquals(StateMutabilityType.Payable, entry.getStateMutability()); + } + + @Test + public void numericEnumStillParses() throws Exception { + ABI abi = mergeAbi("[{\"name\":\"x\",\"stateMutability\":4}]"); + + assertEquals(StateMutabilityType.Payable, abi.getEntrys(0).getStateMutability()); + } + + @Test + public void tokenAtRawLimitStillParses() throws Exception { + String token = repeat('0', JsonFormat.MAX_INTEGER_TOKEN_LENGTH - 1) + "4"; + + ABI abi = mergeAbi("[{\"name\":\"x\",\"stateMutability\":" + token + "}]"); + + assertEquals(JsonFormat.MAX_INTEGER_TOKEN_LENGTH, token.length()); + assertEquals(StateMutabilityType.Payable, abi.getEntrys(0).getStateMutability()); + } + + @Test + public void tokenOverRawLimitIsRejectedWithoutEcho() { + String token = repeat('0', JsonFormat.MAX_INTEGER_TOKEN_LENGTH) + "4"; + + JsonFormat.ParseException error = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"name\":\"x\",\"stateMutability\":" + token + "}]")); + + assertTrue(error.getMessage().contains("Integer token is too long")); + assertBoundedWithoutRepeatedInput(error.getMessage(), '0'); + } + + @Test + public void rawLimitRunsBeforeUnsignedSignValidation() { + String token = "-" + repeat('9', 100_000); + + NumberFormatException error = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseUInt64(token)); + + assertEquals("Integer token is too long: length 100001, max 256", error.getMessage()); + assertBoundedWithoutRepeatedInput(error.getMessage(), '9'); + } + + @Test + public void unsignedNegativeErrorsDoNotReflectToken() { + String token = "-" + repeat('9', 100); + + NumberFormatException uint32Error = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseUInt32(token)); + NumberFormatException uint64Error = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseUInt64(token)); + + assertEquals("Number must be positive.", uint32Error.getMessage()); + assertEquals("Number must be positive.", uint64Error.getMessage()); + assertBoundedWithoutRepeatedInput(uint32Error.getMessage(), '9'); + assertBoundedWithoutRepeatedInput(uint64Error.getMessage(), '9'); + } + + @Test + public void rawLimitIncludesRadixPrefixAndInnerSign() { + String atLimit = "0x-" + repeat('0', JsonFormat.MAX_INTEGER_TOKEN_LENGTH - 4) + "1"; + String overLimit = "0x-" + repeat('0', JsonFormat.MAX_INTEGER_TOKEN_LENGTH - 3) + "1"; + + assertEquals(JsonFormat.MAX_INTEGER_TOKEN_LENGTH, atLimit.length()); + assertEquals(-1L, JsonFormat.parseInt64(atLimit)); + NumberFormatException error = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64(overLimit)); + assertTrue(error.getMessage().contains("Integer token is too long")); + } + + @Test + public void signedInt64ExactBoundariesArePreserved() { + assertEquals(Long.MAX_VALUE, JsonFormat.parseInt64("9223372036854775807")); + assertEquals(Long.MIN_VALUE, JsonFormat.parseInt64("-9223372036854775808")); + + NumberFormatException aboveMax = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64("9223372036854775808")); + NumberFormatException belowMin = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64("-9223372036854775809")); + + assertEquals("Number out of range for 64-bit signed integer.", aboveMax.getMessage()); + assertEquals("Number out of range for 64-bit signed integer.", belowMin.getMessage()); + } + + @Test + public void decimalRangeErrorsAreBounded() { + String twentyDigitValue = repeat('9', 20); + String twentyOneDigitValue = repeat('9', 21); + NumberFormatException twentyDigitError = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64(twentyDigitValue)); + NumberFormatException twentyOneDigitError = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64(twentyOneDigitValue)); + + String expectedMessage = "Number out of range for 64-bit signed integer."; + assertOutOfRangeWithoutRun(twentyDigitError, expectedMessage, '9'); + assertOutOfRangeWithoutRun(twentyOneDigitError, expectedMessage, '9'); + } + + @Test + public void octalRangeErrorsAreBounded() { + String twentyTwoDigitValue = "0" + repeat('7', 22); + String twentyThreeDigitValue = "0" + repeat('7', 23); + NumberFormatException twentyTwoDigitError = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64(twentyTwoDigitValue)); + NumberFormatException twentyThreeDigitError = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64(twentyThreeDigitValue)); + + String expectedMessage = "Number out of range for 64-bit signed integer."; + assertOutOfRangeWithoutRun(twentyTwoDigitError, expectedMessage, '7'); + assertOutOfRangeWithoutRun(twentyThreeDigitError, expectedMessage, '7'); + } + + @Test + public void hexadecimalRangeErrorsAreBounded() { + String sixteenDigitValue = "0x" + repeat('F', 16); + String seventeenDigitValue = "0x" + repeat('F', 17); + NumberFormatException sixteenDigitError = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64(sixteenDigitValue)); + NumberFormatException seventeenDigitError = assertThrows(NumberFormatException.class, + () -> JsonFormat.parseInt64(seventeenDigitValue)); + + String expectedMessage = "Number out of range for 64-bit signed integer."; + assertOutOfRangeWithoutRun(sixteenDigitError, expectedMessage, 'F'); + assertOutOfRangeWithoutRun(seventeenDigitError, expectedMessage, 'F'); + } + + @Test + public void enumFieldsAndUnknownFieldsUseTheGuardedParser() { + String value = repeat('9', 21); + + JsonFormat.ParseException typeError = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"name\":\"x\",\"type\":" + value + "}]")); + JsonFormat.ParseException mutabilityError = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"name\":\"x\",\"stateMutability\":" + value + "}]")); + JsonFormat.ParseException unknownError = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"unknown\":" + value + "}]")); + + String enumMessage = "Number out of range for 32-bit signed integer."; + String unknownFieldMessage = "Number out of range for 64-bit signed integer."; + assertWrappedOutOfRangeWithoutRun(typeError, enumMessage, '9'); + assertWrappedOutOfRangeWithoutRun(mutabilityError, enumMessage, '9'); + assertWrappedOutOfRangeWithoutRun(unknownError, unknownFieldMessage, '9'); + } + + @Test + public void invalidTrailingCharactersRemainSyntaxErrorsInBothParserBranches() { + String[] values = { + repeat('9', 14) + "z", // Long.parseLong branch: numberText length is below 16. + repeat('9', 19) + "z", // BigInteger branch. + repeat('9', 20) + "z", + "0" + repeat('7', 21) + "z", + "0" + repeat('7', 22) + "z", + "0x" + repeat('F', 15) + "z", + "0x" + repeat('F', 16) + "z" + }; + + for (String value : values) { + JsonFormat.ParseException error = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"unknown\":" + value + "}]")); + + assertTrue(error.getMessage().contains("Couldn't parse integer")); + assertFalse(error.getMessage().contains( + "Number out of range for 64-bit signed integer.")); + assertTrue("Unexpectedly long error message", error.getMessage().length() < 512); + } + } + + @Test + public void signsAndRadixPrefixesKeepTheirExistingSemantics() throws Exception { + assertEquals(-Long.MAX_VALUE, JsonFormat.parseInt64("0x-7FFFFFFFFFFFFFFF")); + assertEquals(-1L, JsonFormat.parseInt64("0x-0000000000000001")); + assertEquals(Long.MAX_VALUE, JsonFormat.parseInt64("-0x-7FFFFFFFFFFFFFFF")); + + mergeAbi("[{\"unknown\":0x-7FFFFFFFFFFFFFFF}]"); + mergeAbi("[{\"unknown\":0x-0000000000000001}]"); + mergeAbi("[{\"unknown\":-0x-7FFFFFFFFFFFFFFF}]"); + + ABI abi = mergeAbi("[{\"name\":\"x\",\"stateMutability\":+" + + repeat('0', 20) + "4}]"); + assertEquals(StateMutabilityType.Payable, abi.getEntrys(0).getStateMutability()); + } + + @Test + public void unicodeLeadingZerosKeepBigIntegerCompatibility() { + String value = repeat((char) 0x0660, 20) + "4"; + + assertEquals(4L, JsonFormat.parseInt64(value)); + } + + @Test + public void largeAbiIntegerIsRejectedWithBoundedMessage() { + String value = repeat('9', 100_000); + + JsonFormat.ParseException error = assertThrows(JsonFormat.ParseException.class, + () -> mergeAbi("[{\"unknown\":" + value + "}]")); + + assertTrue(error.getMessage().contains("Integer token is too long")); + assertBoundedWithoutRepeatedInput(error.getMessage(), '9'); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java index 46d1743c5b9..cba524f6881 100644 --- a/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.common.base.Strings; import com.google.protobuf.ByteString; import com.google.protobuf.UnknownFieldSet; @@ -257,6 +258,34 @@ public void testParseInteger() throws Exception { assertTrue(cause instanceof NumberFormatException); } + @Test + public void testFloatParseErrorDoesNotForwardInput() { + Protocol.MetricsInfo.RateInfo.Builder builder = + Protocol.MetricsInfo.RateInfo.newBuilder(); + String value = Strings.repeat("9", 100_000) + "q"; + + JsonFormat.ParseException error = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"meanRate\":" + value + "}", builder, false)); + + assertTrue(error.getMessage().matches( + "^\\d+:\\d+: Couldn't parse number: token length \\d+$")); + assertFalse(error.getMessage().contains("For input string")); + assertFalse(error.getMessage().contains(Strings.repeat("9", 8))); + assertTrue("Unexpectedly long error message", error.getMessage().length() < 128); + } + + @Test + public void testTruncateDoesNotSplitSurrogatePair() throws Exception { + Method privateMethod = JsonFormat.class.getDeclaredMethod("truncate", String.class, int.class); + privateMethod.setAccessible(true); + String supplementaryCharacter = new String(Character.toChars(0x1F600)); + String text = "A" + supplementaryCharacter + "B"; + + assertEquals("A...(truncated)", privateMethod.invoke(null, text, 2)); + assertEquals("A" + supplementaryCharacter + "...(truncated)", + privateMethod.invoke(null, text, 3)); + } + /* * Compatibility-preserved behavior: these cases pass before and after this fix. * They guard unknown-field skipping, repeated-field syntax, and accepted depth. diff --git a/framework/src/test/java/org/tron/core/services/http/JsonFormatTestSupport.java b/framework/src/test/java/org/tron/core/services/http/JsonFormatTestSupport.java new file mode 100644 index 00000000000..20c8540b8a2 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonFormatTestSupport.java @@ -0,0 +1,32 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.common.base.Strings; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; + +final class JsonFormatTestSupport { + + private static final int MAX_ERROR_MESSAGE_LENGTH = 512; + private static final int REFLECTED_INPUT_RUN_LENGTH = 8; + + private JsonFormatTestSupport() { + } + + static String repeat(char value, int count) { + return Strings.repeat(String.valueOf(value), count); + } + + static ABI mergeAbi(String entries) throws JsonFormat.ParseException { + ABI.Builder builder = ABI.newBuilder(); + JsonFormat.merge("{\"entrys\":" + entries + "}", builder, false); + return builder.build(); + } + + static void assertBoundedWithoutRepeatedInput(String message, char input) { + assertTrue("Unexpectedly long error message", message.length() < MAX_ERROR_MESSAGE_LENGTH); + assertFalse("Error message contains the input prefix", + message.contains(repeat(input, REFLECTED_INPUT_RUN_LENGTH))); + } +}