From 0f604c65e025c0b449bee708d5b3b66d3a390a3b Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Wed, 11 Nov 2026 15:01:01 +0800 Subject: [PATCH] feat(api): reject request strings that cannot be encoded as UTF-8 --- .../http/servlets/DeployContractServlet.java | 4 ++ .../services/http/servlets/JsonFormat.java | 25 ++++++- .../services/jsonrpc/TronJsonRpcImpl.java | 3 + .../tron/core/jsonrpc/JsonrpcServiceTest.java | 24 +++++++ .../servlets/JsonFormatUnicodeErrorTest.java | 65 ++++++++++++++++++- 5 files changed, 119 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/http/servlets/DeployContractServlet.java b/framework/src/main/java/org/tron/core/services/http/servlets/DeployContractServlet.java index 8aed6f71306..23afab900d7 100644 --- a/framework/src/main/java/org/tron/core/services/http/servlets/DeployContractServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/servlets/DeployContractServlet.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Component; import org.tron.common.utils.ByteArray; import org.tron.core.Wallet; +import org.tron.core.exception.InvalidHttpParameterException; import org.tron.core.services.http.HttpApi; import org.tron.core.services.http.HttpApi.Access; import org.tron.core.services.http.HttpApi.Surface; @@ -78,6 +79,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } String name = jsonObject.getString("name"); if (!Strings.isNullOrEmpty(name)) { + if (JsonFormat.hasUnpairedSurrogate(name)) { + throw new InvalidHttpParameterException("Invalid name: contains an unpaired surrogate"); + } smartBuilder.setName(name); } diff --git a/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java b/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java index 2b32c75b096..81cc892540c 100644 --- a/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java +++ b/framework/src/main/java/org/tron/core/services/http/servlets/JsonFormat.java @@ -29,6 +29,7 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import com.google.common.base.Utf8; import com.google.common.collect.ImmutableSet; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; @@ -767,7 +768,7 @@ private static Object handlePrimitive(Tokenizer tokenizer, FieldDescriptor field break; case STRING: - value = tokenizer.consumeString(); + value = consumeUtf8String(tokenizer, field); break; case BYTES: @@ -1079,6 +1080,28 @@ private static String replaceMalformedSurrogates(String value) { return new String(value.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); } + /** + * Returns whether the value cannot be encoded as UTF-8, which a protobuf string field requires. + */ + public static boolean hasUnpairedSurrogate(String value) { + try { + Utf8.encodedLength(value); + return false; + } catch (IllegalArgumentException e) { + return true; + } + } + + private static String consumeUtf8String(Tokenizer tokenizer, FieldDescriptor field) + throws ParseException { + String value = tokenizer.consumeString(); + if (hasUnpairedSurrogate(value)) { + throw tokenizer.parseExceptionPreviousToken( + "String field " + field.getFullName() + " contains an unpaired surrogate"); + } + return value; + } + /** * Is this an octal digit. */ diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index eb724633d60..271e5ab0a00 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -1245,6 +1245,9 @@ private TransactionJson buildCreateSmartContractTransaction(byte[] ownerAddress, ByteString.copyFrom(ByteArray.fromHexString(args.resolveData()))); if (StringUtils.isNotEmpty(args.getName())) { + if (JsonFormat.hasUnpairedSurrogate(args.getName())) { + throw new JsonRpcInvalidParamsException("invalid name"); + } smartBuilder.setName(args.getName()); } diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index 3a4aa89ac29..224944dd6a3 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -2011,6 +2011,30 @@ public void testBuildTransactionRejectsDeeplyNestedAbi() { Assert.assertEquals("invalid abi", e.getMessage()); } + @Test + public void testBuildTransactionRejectsUnpairedSurrogateName() { + BuildArguments args = new BuildArguments(); + args.setFrom("0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + args.setData("60806040"); + args.setName("a" + (char) 0xD800); + + JsonRpcInvalidParamsException e = Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.buildTransaction(args)); + Assert.assertEquals("invalid name", e.getMessage()); + } + + @Test + public void testBuildTransactionRejectsUnpairedSurrogateInAbi() { + BuildArguments args = new BuildArguments(); + args.setFrom("0xabd4b9367799eaa3197fecb144eb71de1e049abc"); + args.setData("60806040"); + args.setAbi("[{\"name\":\"\\ud800\"}]"); + + JsonRpcInvalidParamsException e = Assert.assertThrows(JsonRpcInvalidParamsException.class, + () -> tronJsonRpc.buildTransaction(args)); + Assert.assertEquals("invalid abi", e.getMessage()); + } + @Test public void testWeb3ClientVersion() { try { diff --git a/framework/src/test/java/org/tron/core/services/http/servlets/JsonFormatUnicodeErrorTest.java b/framework/src/test/java/org/tron/core/services/http/servlets/JsonFormatUnicodeErrorTest.java index 7a0a5bc7265..9abaf273de4 100644 --- a/framework/src/test/java/org/tron/core/services/http/servlets/JsonFormatUnicodeErrorTest.java +++ b/framework/src/test/java/org/tron/core/services/http/servlets/JsonFormatUnicodeErrorTest.java @@ -1,6 +1,7 @@ package org.tron.core.services.http.servlets; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -19,10 +20,60 @@ import org.tron.core.services.ratelimiter.RateLimiterContainer; import org.tron.json.JSONObject; import org.tron.protos.Protocol.Account; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; public class JsonFormatUnicodeErrorTest { private static final String EMOJI = new String(Character.toChars(0x1F600)); + private static final String NAME_SURROGATE_ERROR = + "1:9: String field protocol.SmartContract.name contains an unpaired surrogate"; + + @Test + public void testUnpairedSurrogateDetection() { + assertTrue(JsonFormat.hasUnpairedSurrogate(String.valueOf((char) 0xD800))); + assertTrue(JsonFormat.hasUnpairedSurrogate(String.valueOf((char) 0xDC00))); + assertTrue(JsonFormat.hasUnpairedSurrogate("" + (char) 0xDC00 + (char) 0xD800)); + assertTrue(JsonFormat.hasUnpairedSurrogate("a" + (char) 0xD83D)); + assertFalse(JsonFormat.hasUnpairedSurrogate("")); + assertFalse(JsonFormat.hasUnpairedSurrogate(EMOJI)); + assertFalse(JsonFormat.hasUnpairedSurrogate("" + (char) 0x4E2D + (char) 0xFFFF)); + } + + @Test + public void testStringFieldRejectsUnpairedSurrogates() { + assertNameRejected("\\ud800"); + assertNameRejected("\\udc00"); + assertNameRejected("a\\udc00\\ud800"); + assertNameRejected("x\\uD83D"); + assertNameRejected(String.valueOf((char) 0xD800)); + } + + @Test + public void testStringFieldKeepsWellFormedText() throws Exception { + SmartContract.Builder builder = SmartContract.newBuilder(); + JsonFormat.merge("{\"name\":\"\\ud83d\\ude00\\u4e2d\\uffff" + EMOJI + "\"}", builder, false); + + assertEquals(EMOJI + (char) 0x4E2D + (char) 0xFFFF + EMOJI, builder.getName()); + } + + @Test + public void testDeployContractRejectsUnpairedSurrogateInAbi() throws Exception { + String body = "{\"owner_address\":\"\",\"abi\":\"[{\\\"name\\\":\\\"\\\\ud800\\\"}]\"}"; + + String error = requestError(new DeployContractServlet(), "/wallet/deploycontract", body); + + assertEquals("1:20: String field protocol.SmartContract.ABI.Entry.name" + + " contains an unpaired surrogate", error); + } + + @Test + public void testDeployContractRejectsUnpairedSurrogateInName() throws Exception { + String body = "{\"owner_address\":\"\",\"bytecode\":\"00\",\"name\":\"\\ud800\"}"; + + String error = requestError(new DeployContractServlet(), "/wallet/deploycontract", body); + + assertEquals("Invalid name: contains an unpaired surrogate", error); + } @Test public void testInvalidSupplementaryEscapeKeepsLegacyReplacement() { @@ -129,6 +180,11 @@ public void testOrdinaryIntegerErrorsKeepTheirMessages() { private static String requestError(RateLimiterServlet servlet, String path, JSONObject input) throws Exception { + return requestError(servlet, path, input.toJSONString()); + } + + private static String requestError(RateLimiterServlet servlet, String path, String requestBody) + throws Exception { CommonParameter args = Args.getInstance(); long originalMaxSize = args.getHttpMaxMessageSize(); boolean originalNonBlocking = args.isRateLimiterApiNonBlocking(); @@ -145,7 +201,7 @@ private static String requestError(RateLimiterServlet servlet, String path, JSON args.setHttpMaxMessageSize(1_000_000L); args.setRateLimiterApiNonBlocking(false); server.start(); - byte[] body = input.toJSONString().getBytes(StandardCharsets.UTF_8); + byte[] body = requestBody.getBytes(StandardCharsets.UTF_8); byte[] headers = ("POST " + path + " HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" @@ -177,6 +233,13 @@ private static String requestError(RateLimiterServlet servlet, String path, JSON } } + private static void assertNameRejected(String text) { + JsonFormat.ParseException error = assertThrows(JsonFormat.ParseException.class, + () -> JsonFormat.merge("{\"name\":\"" + text + "\"}", SmartContract.newBuilder(), false)); + + assertEquals(NAME_SURROGATE_ERROR, error.getMessage()); + } + private static void assertEscapeError(String input, String expectedCharacter) { JsonFormat.InvalidEscapeSequence error = assertThrows(JsonFormat.InvalidEscapeSequence.class, () -> JsonFormat.unescapeText(input));