Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 76 additions & 25 deletions framework/src/main/java/org/tron/core/services/http/JsonFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Class<? extends Message>> MESSAGES = ImmutableSet.of(
BalanceContract.AccountBalanceResponse.class,
Expand Down Expand Up @@ -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);
Expand All @@ -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) + "\".");
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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));
}
}
}
Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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();
}
}

Expand Down Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading
Loading