diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/StringUtils.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/StringUtils.java index 959aa7304caa6..be9b840d7ed34 100644 --- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/StringUtils.java +++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/StringUtils.java @@ -42,6 +42,18 @@ public class StringUtils { static final int WILD_COMPARE_NO_MATCH = -1; + /** + * Maximum absolute {@link BigDecimal#scale()} accepted by {@link + * #consistentToString(BigDecimal)} before it falls back to scientific notation. + * + *

{@link BigDecimal#toPlainString()} expands the value to a non-scientific decimal string + * whose length grows with {@code |scale|}. A value such as {@code 1e1000000000} would therefore + * materialize a ~1GB {@link String}, causing {@link OutOfMemoryError} / denial of service on the + * client. Any scale beyond this bound is rejected for expansion and the caller gets the scientific + * form instead, which is a few dozen characters at most. + */ + static final int MAX_PLAIN_STRING_SCALE = 10_000; + static { for (int i = -128; i <= 127; i++) { allBytes[i - -128] = (byte) i; @@ -63,9 +75,16 @@ public static String consistentToString(BigDecimal decimal) { if (decimal == null) { return null; } + // Guard against CVE-style DoS: BigDecimal values constructed from extreme scientific + // notation (e.g. "1e1000000000") have a huge |scale| and would otherwise cause + // toPlainString() to allocate a multi-GB String and OOM the JVM. Fall back to the + // scientific form, which is always short. + if (decimal.scale() > MAX_PLAIN_STRING_SCALE || decimal.scale() < -MAX_PLAIN_STRING_SCALE) { + return decimal.toString(); + } if (toPlainStringMethod != null) { try { - return (String) toPlainStringMethod.invoke(decimal, null); + return (String) toPlainStringMethod.invoke(decimal, (Object[]) null); } catch (InvocationTargetException | IllegalAccessException e) { LOGGER.warn("consistent to String Error:", e); } diff --git a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/StringUtilsTest.java b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/StringUtilsTest.java new file mode 100644 index 0000000000000..d154765734311 --- /dev/null +++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/StringUtilsTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.jdbc; + +import org.junit.Test; + +import java.math.BigDecimal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class StringUtilsTest { + + @Test + public void consistentToStringHandlesNull() { + assertNull(StringUtils.consistentToString(null)); + } + + @Test + public void consistentToStringExpandsNormalDecimal() { + assertEquals("123.45", StringUtils.consistentToString(new BigDecimal("123.45"))); + // Plain-string form, not scientific notation. + assertEquals("100", StringUtils.consistentToString(new BigDecimal("1E+2"))); + } + + @Test + public void consistentToStringExpandsUpToLimit() { + // scale right at the boundary must still be expanded to plain form. + BigDecimal atLimit = new BigDecimal("1E+" + StringUtils.MAX_PLAIN_STRING_SCALE); + String out = StringUtils.consistentToString(atLimit); + assertEquals(StringUtils.MAX_PLAIN_STRING_SCALE + 1, out.length()); + assertTrue(out.startsWith("1")); + } + + /** + * Regression test for the BigDecimal DoS: {@code new BigDecimal("1e1000000000")} used to cause + * {@link BigDecimal#toPlainString()} to allocate a ~1GB string and OOM the JVM. The guard must + * refuse to expand such values and return the compact scientific form instead. + */ + @Test + public void consistentToStringRefusesToExpandHugeScale() { + BigDecimal malicious = new BigDecimal("1e1000000000"); + String out = StringUtils.consistentToString(malicious); + // Must be short (scientific notation), never the ~1e9-char plain form. + assertTrue( + "expected scientific form, got length " + out.length(), + out.length() < 64); + assertTrue(out.contains("E") || out.contains("e")); + } + + @Test + public void consistentToStringRefusesToExpandHugePositiveScale() { + // Very large positive scale -> also produces a long plain string; must be refused. + BigDecimal malicious = new BigDecimal("1e-1000000000"); + String out = StringUtils.consistentToString(malicious); + assertTrue( + "expected scientific form, got length " + out.length(), + out.length() < 64); + } +}