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))); + } +}