From 395466a2c1ad13f11ad9d532445710825b3d7d39 Mon Sep 17 00:00:00 2001 From: halibobo1205 Date: Fri, 28 Aug 2026 17:52:48 +0800 Subject: [PATCH] feat(api): sanitize HTTP API error responses Standard HTTP error paths used to expose internal details to clients: Util.processError prefixed every message with the Java exception class name, several servlets printed raw Throwable.getMessage() directly, and the two solidity query endpoints returned bare-text error bodies. Centralize the client-facing text decision in Util.processError: * keep the raw non-blank message only for the exact runtime types JsonFormat.ParseException, ContractValidateException and MaintenanceUnavailableException; a null, empty or whitespace-only message falls back to "internal server error" * keep two fixed messages only on exact type + exact text match: IllegalAccessException("lack of computing resources") and IllegalArgumentException(EVENTS_DEPRECATED_MSG) * return {"Error":"internal server error"} for every other exception, with no exception class name Client-visible changes: * all processError-based error bodies lose the "class : " prefix; unclassified raw messages become "internal server error" * the rate-limit rejection body becomes {"Error":"lack of computing resources"} on every endpoint extending RateLimiterServlet, including /jsonrpc (no JSON-RPC code is modified) * gettransactionbyid / gettransactioninfobyid on solidity return standard {"Error":...} JSON instead of bare text * validateaddress, getBrokerage and getReward replace leaked library messages in their failure branches with existing fixed texts; the "INVALID address" body is now written via processError and loses the space after the colon * getblock keeps its exact error bodies (refactor only) HTTP status codes, success responses, request validation rules and gRPC behavior are unchanged. --- .../core/services/http/GetBlockServlet.java | 4 +- .../services/http/GetBrokerageServlet.java | 8 +- .../core/services/http/GetBurnTrxServlet.java | 8 +- .../services/http/GetNodeInfoServlet.java | 8 +- .../services/http/GetPendingSizeServlet.java | 8 +- .../core/services/http/GetRewardServlet.java | 15 +--- .../GetTransactionInfoByBlockNumServlet.java | 15 +--- .../services/http/RateLimiterServlet.java | 3 +- .../org/tron/core/services/http/Util.java | 44 +++++++-- .../services/http/ValidateAddressServlet.java | 2 +- .../GetTransactionByIdSolidityServlet.java | 14 +-- ...GetTransactionInfoByIdSolidityServlet.java | 15 +--- .../services/http/BroadcastServletTest.java | 2 +- .../services/http/UtilProcessErrorTest.java | 90 +++++++++++++++++++ ...GetTransactionByIdSolidityServletTest.java | 4 +- 15 files changed, 147 insertions(+), 93 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java diff --git a/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java index 2320fc87c7d..b0a71fa8455 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java @@ -77,9 +77,7 @@ private void fillResponse(boolean visible, BlockReq request, HttpServletResponse response.getWriter().println("{}"); } } catch (IllegalArgumentException e) { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("Error", e.getMessage()); - response.getWriter().println(jsonObject.toJSONString()); + Util.processError(e.getMessage(), response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java index 1fbd94fe690..7e2e082e70f 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -27,12 +26,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { } response.getWriter().println("{\"brokerage\": " + value + "}"); } catch (DecoderException | IllegalArgumentException e) { - try { - response.getWriter() - .println("{\"Error\": " + "\"INVALID address, " + e.getMessage() + "\"}"); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(Util.INVALID_ADDRESS_MSG, response); } catch (Exception e) { Util.processError(e, response); } diff --git a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java index ea066a6e98c..26740ffed0f 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -24,12 +23,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { : "{\"burnTrxAmount\": " + value + "}"; response.getWriter().println(out); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java b/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java index 0b8f7b9ce2b..b1746e1ff1f 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -24,12 +23,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println(JSON.toJSONString(nodeInfo)); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java index 9788c926586..6369600352d 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -24,12 +23,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { : "{\"pendingSize\": " + value + "}"; response.getWriter().println(out); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java index 61b88d1160f..31327397aab 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -29,19 +28,9 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { : "{\"reward\": " + value + "}"; response.getWriter().println(out); } catch (DecoderException | IllegalArgumentException e) { - try { - response.getWriter() - .println("{\"Error\": " + "\"INVALID address, " + e.getMessage() + "\"}"); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(Util.INVALID_ADDRESS_MSG, response); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java index 5d0a09b1a68..25998c909b6 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -52,12 +51,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println("{}"); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } @@ -75,12 +69,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.getWriter().println("{}"); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } } diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index b5ae7d58623..0fffa1340c9 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -131,8 +131,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) super.service(req, resp); Metrics.histogramObserve(requestTimer); } else { - resp.getWriter() - .println(Util.printErrorMsg(new IllegalAccessException("lack of computing resources"))); + Util.processError(Util.RATE_LIMITER_ERROR_MSG, resp); } } catch (ServletException | IOException | BadMessageException e) { throw e; diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 5be2495e1f7..8b9445b22a5 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -48,6 +48,8 @@ import org.tron.core.capsule.TransactionCapsule; import org.tron.core.config.args.Args; import org.tron.core.db.TransactionTrace; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.MaintenanceUnavailableException; import org.tron.core.services.http.JsonFormat.ParseException; import org.tron.json.JSON; import org.tron.json.JSONArray; @@ -65,6 +67,10 @@ @Slf4j(topic = "API") public class Util { + private static final String INTERNAL_SERVER_ERROR = "internal server error"; + public static final String RATE_LIMITER_ERROR_MSG = "lack of computing resources"; + static final String INVALID_ADDRESS_MSG = "INVALID address"; + public static final String EVENTS_DEPRECATED_MSG = "'events' field is deprecated and no longer supported"; @@ -114,12 +120,35 @@ public static String printTransactionFee(String transactionFee) { return jsonObject.toJSONString(); } - public static String printErrorMsg(Exception e) { + public static String printErrorMsg(String msg) { JSONObject jsonObject = new JSONObject(); - jsonObject.put("Error", e.getClass() + " : " + e.getMessage()); + jsonObject.put("Error", msg); return jsonObject.toJSONString(); } + private static String clientMessage(Exception e) { + if (e == null) { + return INTERNAL_SERVER_ERROR; + } + + Class type = e.getClass(); + if (type == IllegalAccessException.class) { + return RATE_LIMITER_ERROR_MSG.equals(e.getMessage()) + ? RATE_LIMITER_ERROR_MSG : INTERNAL_SERVER_ERROR; + } + if (type == IllegalArgumentException.class) { + return EVENTS_DEPRECATED_MSG.equals(e.getMessage()) + ? EVENTS_DEPRECATED_MSG : INTERNAL_SERVER_ERROR; + } + if (type == ParseException.class + || type == ContractValidateException.class + || type == MaintenanceUnavailableException.class) { + String message = e.getMessage(); + return StringUtils.isBlank(message) ? INTERNAL_SERVER_ERROR : message; + } + return INTERNAL_SERVER_ERROR; + } + public static String printBlockList(BlockList list, boolean selfType) { List blocks = list.getBlockList(); JSONObject jsonObject = new JSONObject(); @@ -526,11 +555,16 @@ public static String getMemo(byte[] memo) { } public static void processError(Exception e, HttpServletResponse response) { - logger.debug(e.getMessage(), e); + logger.debug("HTTP request failed", e); + processError(clientMessage(e), response); + } + + // Bypasses clientMessage: callers must pass audited fixed or pre-existing client texts only. + static void processError(String msg, HttpServletResponse response) { try { - response.getWriter().println(Util.printErrorMsg(e)); + response.getWriter().println(Util.printErrorMsg(msg)); } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); + logger.debug("Failed to write HTTP error response", ioe); } } diff --git a/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java b/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java index 07eecfc5466..3ef45b42a7e 100644 --- a/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java @@ -47,7 +47,7 @@ private String validAddress(String input) { } } catch (Exception e) { result = false; - msg = e.getMessage(); + msg = "Invalid address"; } JSONObject jsonAddress = new JSONObject(); diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java index f98c7450afc..5998bc0850f 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java @@ -30,12 +30,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { String input = request.getParameter("value"); fillResponse(ByteString.copyFrom(ByteArray.fromHexString(input)), visible, response); } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } @@ -46,12 +41,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) JsonFormat.merge(params.getParams(), build, params.isVisible()); fillResponse(build.build().getValue(), params.isVisible(), response); } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java index 0408215f09d..197f5aaec0d 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java @@ -1,7 +1,6 @@ package org.tron.core.services.http.solidity; import com.google.protobuf.ByteString; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -37,12 +36,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println(JsonFormat.printToString(transInfo, visible)); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } @@ -60,12 +54,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.getWriter().println(JsonFormat.printToString(transInfo, params.isVisible())); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java b/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java index d6bf3850f30..532ddcd5521 100644 --- a/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java @@ -156,7 +156,7 @@ public void doPostTest() throws IOException { while ((text = bufferedReader.readLine()) != null) { sb.append(text); } - Assert.assertTrue(sb.toString().contains("null")); + Assert.assertTrue(sb.toString().contains("{\"Error\":\"internal server error\"}")); httpUrlConnection.disconnect(); } } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java b/framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java new file mode 100644 index 00000000000..e1701faa129 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java @@ -0,0 +1,90 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.bouncycastle.util.encoders.DecoderException; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.HeaderNotFound; +import org.tron.core.exception.MaintenanceUnavailableException; +import org.tron.core.exception.ZkProofValidateException; +import org.tron.json.JSONException; +import org.tron.json.JSONObject; + +public class UtilProcessErrorTest { + + private static final String INTERNAL_SERVER_ERROR = "internal server error"; + private static final String RATE_LIMITER_ERROR_MSG = "lack of computing resources"; + + @Test + public void exactCompatibilityTypesPreserveNonBlankMessage() throws Exception { + assertError(new JsonFormat.ParseException("1:2: invalid \"field\"\nvalue"), + "1:2: invalid \"field\"\nvalue"); + assertError(new ContractValidateException("balance is not sufficient"), + "balance is not sufficient"); + assertError(new MaintenanceUnavailableException("maintenance in progress"), + "maintenance in progress"); + } + + @Test + public void unclassifiedTypesFailClosed() throws Exception { + DecoderException decoder = assertThrows(DecoderException.class, () -> Hex.decode("zz")); + Exception[] errors = { + new NullPointerException("internal field name"), + new JSONException("server serialization detail"), + new InvalidProtocolBufferException("stored protobuf detail"), + decoder, + new HeaderNotFound("latest block not found"), + new IllegalArgumentException("No enum constant internal.Type.VALUE"), + new ZkProofValidateException("wrapped validation detail", true) + }; + + for (Exception error : errors) { + assertError(error, INTERNAL_SERVER_ERROR); + } + } + + @Test + public void onlyExactFixedControlSignalsArePreserved() throws Exception { + assertError(new IllegalAccessException(RATE_LIMITER_ERROR_MSG), RATE_LIMITER_ERROR_MSG); + assertError(new IllegalAccessException("other access failure"), INTERNAL_SERVER_ERROR); + assertError(new IllegalArgumentException(Util.EVENTS_DEPRECATED_MSG), + Util.EVENTS_DEPRECATED_MSG); + assertError(new IllegalArgumentException("other argument failure"), INTERNAL_SERVER_ERROR); + assertError(new NumberFormatException(Util.EVENTS_DEPRECATED_MSG), INTERNAL_SERVER_ERROR); + } + + @Test + public void nullBlankAndSubclassMessagesFailClosed() throws Exception { + assertError(null, INTERNAL_SERVER_ERROR); + assertError(new JsonFormat.ParseException(null), INTERNAL_SERVER_ERROR); + assertError(new JsonFormat.ParseException(" "), INTERNAL_SERVER_ERROR); + assertError(new ContractValidateException("subclass message") { }, INTERNAL_SERVER_ERROR); + } + + @Test + public void stringChannelWritesAuditedTextVerbatim() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + Util.processError(Util.INVALID_ADDRESS_MSG, response); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(Util.INVALID_ADDRESS_MSG, body.getString("Error")); + } + + @Test + public void stringChannelWithNullMessageWritesEmptyObject() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + Util.processError((String) null, response); + assertEquals("{}", response.getContentAsString().trim()); + } + + private static void assertError(Exception error, String expected) throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + Util.processError(error, response); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(expected, body.getString("Error")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java b/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java index e1abb41d1e1..5c4e5a1e8e1 100644 --- a/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java @@ -139,7 +139,7 @@ public void doPostTest() throws IOException { while ((text = bufferedReader.readLine()) != null) { sb.append(text); } - Assert.assertTrue(sb.toString().contains("null")); + Assert.assertTrue(sb.toString().contains("{\"Error\":\"internal server error\"}")); httpUrlConnection.disconnect(); } @@ -195,7 +195,7 @@ public void doGetTest() throws IOException { while ((text = bufferedReader.readLine()) != null) { sb.append(text); } - Assert.assertTrue(sb.toString().contains("null")); + Assert.assertTrue(sb.toString().contains("{\"Error\":\"internal server error\"}")); httpUrlConnection.disconnect(); } }