diff --git a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java index 7e170f9dab5..8b4fa1c9b2c 100644 --- a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java @@ -123,6 +123,7 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode, } long newUsage = getUsage(averageLastUsage, oldWindowSize, averageUsage, this.windowSize); + // remainUsage is the decayed lastUsage long remainUsage = getUsage(averageLastUsage, oldWindowSize); if (remainUsage == 0) { accountCapsule.setNewWindowSizeV2(resourceCode, this.windowSize * WINDOW_SIZE_PRECISION); diff --git a/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java b/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java index c3776921244..df641026770 100644 --- a/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java +++ b/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java @@ -295,13 +295,14 @@ private void resetAccountUsage(AccountCapsule accountCap, } long currentSize = accountCap.getWindowSize(ENERGY); long currentUsage = accountCap.getEnergyUsage(); - // Drop the pre consumed frozen energy + // Drop the preconsumed frozen energy long newArea = currentUsage * currentSize - (mergedUsage * mergedSize - usage * size); // If area merging happened during suicide, use the current window size long newSize = mergedSize == currentSize ? size : currentSize; - // Calc new usage by fixed x-axes - long newUsage = max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath()); + // A zero window size means no valid time window exists and thus zero usage + long newUsage = newSize == 0 ? 0 + : max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath()); // Reset account usage and window size accountCap.setEnergyUsage(newUsage); accountCap.setNewWindowSize(ENERGY, newUsage == 0 ? 0L : newSize); @@ -312,13 +313,14 @@ private void resetAccountUsageV2(AccountCapsule accountCap, long currentSize = accountCap.getWindowSize(ENERGY); long currentSize2 = accountCap.getWindowSizeV2(ENERGY); long currentUsage = accountCap.getEnergyUsage(); - // Drop the pre consumed frozen energy + // Drop the preconsumed frozen energy long newArea = currentUsage * currentSize - (mergedUsage * mergedSize - usage * size); // If area merging happened during suicide, use the current window size long newSize = mergedSize == currentSize ? size : currentSize; long newSize2 = mergedSize == currentSize ? size2 : currentSize2; - // Calc new usage by fixed x-axes - long newUsage = max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath()); + // A zero window size means no valid time window exists and thus zero usage + long newUsage = newSize == 0 ? 0 + : max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath()); // Reset account usage and window size accountCap.setEnergyUsage(newUsage); accountCap.setNewWindowSizeV2(ENERGY, newUsage == 0 ? 0L : newSize2); 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 7a66aed34f6..395d3f46fed 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 @@ -3,7 +3,9 @@ import com.google.common.base.Strings; import io.prometheus.client.Histogram; import java.io.IOException; -import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -31,7 +33,25 @@ @Slf4j public abstract class RateLimiterServlet extends HttpServlet { private static final String KEY_PREFIX_HTTP = "http_"; - private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter."; + + // Whitelist of allowed rate limiter adapter class names. + // Keys are derived from Class.getSimpleName() so they stay in sync if classes are renamed. + // Class.forName() is intentionally NOT used; only these pre-approved classes may be loaded. + private static final Map> ALLOWED_ADAPTERS; + private static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName(); + + static { + Map> m = new HashMap<>(); + for (Class c : new Class[]{ + GlobalPreemptibleAdapter.class, + QpsRateLimiterAdapter.class, + IPQPSRateLimiterAdapter.class, + DefaultBaseQqsAdapter.class + }) { + m.put(c.getSimpleName(), c); + } + ALLOWED_ADAPTERS = Collections.unmodifiableMap(m); + } @Autowired private RateLimiterContainer container; @@ -40,42 +60,22 @@ public abstract class RateLimiterServlet extends HttpServlet { private void addRateContainer() { RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance() .getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName()); - boolean success = false; final String name = getClass().getSimpleName(); - if (item != null) { - String cName = ""; - String params = ""; - Object obj; - try { - cName = item.getStrategy(); - params = item.getParams(); - // add the specific rate limiter strategy of servlet. - Class c = Class.forName(ADAPTER_PREFIX + cName); - Constructor constructor; - if (c == GlobalPreemptibleAdapter.class || c == QpsRateLimiterAdapter.class - || c == IPQPSRateLimiterAdapter.class) { - constructor = c.getConstructor(String.class); - obj = constructor.newInstance(params); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } else { - constructor = c.getConstructor(); - obj = constructor.newInstance(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj); - } - success = true; - } catch (Exception e) { - this.throwTronError(cName, params, name, e); - } - } - if (!success) { - // if the specific rate limiter strategy of servlet is not defined or fail to add, - // then add a default Strategy. - try { - IRateLimiter rateLimiter = new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM); - container.add(KEY_PREFIX_HTTP, name, rateLimiter); - } catch (Exception e) { - this.throwTronError("DefaultBaseQqsAdapter", QpsStrategy.DEFAULT_QPS_PARAM, name, e); + + // If no config for this servlet, fall back to the default adapter. + String cName = (item != null) ? item.getStrategy() : DEFAULT_ADAPTER_NAME; + String params = (item != null) ? item.getParams() : QpsStrategy.DEFAULT_QPS_PARAM; + + try { + Class c = ALLOWED_ADAPTERS.get(cName); + if (c == null) { + throw new IllegalArgumentException( + "Unknown rate limiter adapter (not in whitelist): " + cName); } + IRateLimiter rateLimiter = (IRateLimiter) c.getConstructor(String.class).newInstance(params); + container.add(KEY_PREFIX_HTTP, name, rateLimiter); + } catch (Exception e) { + this.throwTronError(cName, params, name, e); } } diff --git a/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java new file mode 100644 index 00000000000..b71efd7d363 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java @@ -0,0 +1,88 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.util.Map; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter; +import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter; +import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; +import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter; + +/** + * Security test: verifies that RateLimiterServlet uses a strict whitelist + * instead of Class.forName(), preventing arbitrary class loading (RCE) + * via a tampered config file. + */ +public class RateLimiterServletWhitelistTest { + + // Derive names from the classes themselves — stays in sync if classes are renamed. + private static final String GLOBAL_PREEMPTIBLE = GlobalPreemptibleAdapter.class.getSimpleName(); + private static final String QPS_RATE_LIMITER = QpsRateLimiterAdapter.class.getSimpleName(); + private static final String IP_QPS_RATE_LIMITER = IPQPSRateLimiterAdapter.class.getSimpleName(); + private static final String DEFAULT_BASE_QPS = DefaultBaseQqsAdapter.class.getSimpleName(); + + private static Map> allowedAdapters; + + @SuppressWarnings("unchecked") + @BeforeClass + public static void loadWhitelist() throws Exception { + Field f = RateLimiterServlet.class.getDeclaredField("ALLOWED_ADAPTERS"); + f.setAccessible(true); + allowedAdapters = (Map>) f.get(null); + } + + // Verifies all 4 legitimate adapters are present and map to the correct classes. + @Test + public void testWhitelistContents() { + assertNotNull(allowedAdapters.get(GLOBAL_PREEMPTIBLE)); + assertTrue(allowedAdapters.get(GLOBAL_PREEMPTIBLE) + .isAssignableFrom(GlobalPreemptibleAdapter.class)); + + assertNotNull(allowedAdapters.get(QPS_RATE_LIMITER)); + assertTrue(allowedAdapters.get(QPS_RATE_LIMITER) + .isAssignableFrom(QpsRateLimiterAdapter.class)); + + assertNotNull(allowedAdapters.get(IP_QPS_RATE_LIMITER)); + assertTrue(allowedAdapters.get(IP_QPS_RATE_LIMITER) + .isAssignableFrom(IPQPSRateLimiterAdapter.class)); + + assertNotNull(allowedAdapters.get(DEFAULT_BASE_QPS)); + assertTrue(allowedAdapters.get(DEFAULT_BASE_QPS) + .isAssignableFrom(DefaultBaseQqsAdapter.class)); + } + + // Verifies that arbitrary / malicious class names are rejected by the whitelist. + @Test + public void testInvalidClassNameIsRejected() { + assertNull(allowedAdapters.get("com.evil.MaliciousAdapter")); + assertNull(allowedAdapters.get("../../../../evil.Payload")); + assertNull(allowedAdapters.get(Runtime.class.getName())); + assertNull(allowedAdapters.get(ProcessBuilder.class.getName())); + assertNull(allowedAdapters.get("")); + assertNull(allowedAdapters.get(null)); + } + + // Verifies the whitelist cannot be modified at runtime (unmodifiable map). + @Test(expected = UnsupportedOperationException.class) + public void testWhitelistIsImmutable() { + allowedAdapters.put("Injected", DefaultBaseQqsAdapter.class); + } + + // Verifies structural invariants: exact size and all entries implement IRateLimiter. + @Test + public void testWhitelistStructure() { + assertFalse("Whitelist must not be empty", allowedAdapters.isEmpty()); + assertTrue("Whitelist must contain exactly 4 adapters", allowedAdapters.size() == 4); + for (Map.Entry> entry : allowedAdapters.entrySet()) { + assertTrue(entry.getKey() + " must implement IRateLimiter", + IRateLimiter.class.isAssignableFrom(entry.getValue())); + } + } +}