From c34ee462c45ff0e087c94029bb2c62d4b15c78b8 Mon Sep 17 00:00:00 2001 From: waynercheung Date: Mon, 20 Jul 2026 21:39:16 +0800 Subject: [PATCH] fix(http): correct JsonFormat string escaping and decoding escapeBytesSelfType escaped only double quotes. Backslashes and raw control characters could therefore produce invalid JSON, or alter the object seen by the lenient re-parse in Util.printTransactionToJSON. Use escapeText for valid UTF-8 and fall back to hex for malformed UTF-8. Unescape visible name-string bytes on input so print and merge are symmetric. Also fix newline-tail duplication in JsonGenerator.print, U+FFFF truncation and malformed-surrogate handling in escapeText, and support the escaped solidus while rejecting signed Unicode escapes in unescapeText. BREAKING CHANGE: for visible=true name-string byte fields, JSON escape sequences are now decoded before conversion to bytes, so a request sending "a\nb" stores one 0x0A byte instead of a backslash followed by 'n'. Such requests already complete today, they just store the undecoded text, so upgrading changes the bytes written on chain for clients that escape their payloads correctly. Malformed or unsupported escapes are now rejected, while the legacy "\'" extension stays accepted. The shared proto string parser also now accepts "\/" and rejects malformed escapes such as "\u+123", in both visible modes; the visible=false bytes/hex path itself is unchanged. During a rolling upgrade, avoid crossing node versions within an affected visible=true create/sign/broadcast flow, in either direction. --- .../tron/core/services/http/JsonFormat.java | 93 +- .../services/http/JsonFormatEscapeTest.java | 796 ++++++++++++++++++ 2 files changed, 864 insertions(+), 25 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/JsonFormatEscapeTest.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 e6ccb4e4d1..f99339c5d8 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 @@ -43,8 +43,6 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT import java.io.IOException; import java.math.BigInteger; import java.nio.CharBuffer; -import java.text.CharacterIterator; -import java.text.StringCharacterIterator; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -58,7 +56,6 @@ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT import org.tron.common.utils.Commons; import org.tron.common.utils.StringUtil; import org.tron.core.Constant; -import org.tron.json.JSON; import org.tron.protos.contract.BalanceContract; /** @@ -82,6 +79,12 @@ public class JsonFormat { = "Writing to a StringBuilder threw an IOException (should never happen)."; private static final String EXPECTED_STRING = "Expected string."; private static final String MISSING_END_QUOTE = "String missing ending quote."; + /** + * What an unpaired surrogate is rendered as. Matches the substitution Java and protobuf apply + * when encoding such a string to UTF-8, so JSON output is the same whether or not the message + * has already been through a protobuf round trip. + */ + private static final char UNPAIRED_SURROGATE_REPLACEMENT = '?'; public static final boolean ALWAYS_OUTPUT_DEFAULT_VALUE_FIELDS = true; public static final Set> MESSAGES = ImmutableSet.of( @@ -866,14 +869,16 @@ static String escapeBytesSelfType(ByteString input, final String fliedName) { } //Normal String if (HttpSelfFormatFieldName.isNameStringFormat(fliedName)) { - String result = new String(input.toByteArray()); - result = result.replaceAll("\"", "\\\\\""); - try { - JSON.parseObject("{\"key\":\"" + result + "\"}"); - return result; - } catch (Exception e) { + // Bytes that are not valid UTF-8 fall back to hex. This keeps the hex representation + // faithful (instead of lossy U+FFFD mojibake produced by a default-charset decode) and + // guarantees escapeText() never sees an unpaired surrogate. Note that hex is not byte + // round-trippable through the inbound unescapeBytesSelfType(copyFromUtf8) path. + if (!input.isValidUtf8()) { return ByteArray.toHexString(input.toByteArray()); } + // Delegate to the same JSON string escaping used for proto string fields, so backslash, + // double quote and every control char 0x00-0x1F are escaped properly. + return escapeText(input.toStringUtf8()); } //HEX return ByteArray.toHexString(input.toByteArray()); @@ -905,7 +910,8 @@ static ByteString unescapeBytes(CharSequence input) throws InvalidEscapeSequence // them. /** - * Implements JSON string escaping as specified here. + * Implements JSON string escaping as specified in + * RFC 8259 (which obsoletes RFC 4627). *