CVE-2025-66453
Description
Rhino is an open-source implementation of JavaScript written entirely in Java. Prior to 1.8.1, 1.7.15.1, and 1.7.14.1, when an application passed an attacker controlled float poing number into the toFixed() function, it might lead to high CPU consumption and a potential Denial of Service. Small numbers go through this call stack: NativeNumber.numTo > DToA.JS_dtostr > DToA.JS_dtoa > DToA.pow5mult where pow5mult attempts to raise 5 to a ridiculous power. This vulnerability is fixed in 1.8.1, 1.7.15.1, and 1.7.14.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.mozilla:rhinoMaven | < 1.7.14.1 | 1.7.14.1 |
org.mozilla:rhinoMaven | >= 1.7.15, < 1.7.15.1 | 1.7.15.1 |
org.mozilla:rhinoMaven | >= 1.8.0, < 1.8.1 | 1.8.1 |
Affected products
3cpe:2.3:a:mozilla:rhino:*:*:*:*:*:*:*:*+ 2 more
- cpe:2.3:a:mozilla:rhino:*:*:*:*:*:*:*:*range: <1.7.14.1
- cpe:2.3:a:mozilla:rhino:1.7.15:*:*:*:*:*:*:*
- cpe:2.3:a:mozilla:rhino:1.8.0:*:*:*:*:*:*:*
Patches
12bcf4c43deacReplace number formatting operations on Number
6 files changed · +310 −111
src/org/mozilla/javascript/dtoa/DecimalFormatter.java+163 −0 added@@ -0,0 +1,163 @@ +package org.mozilla.javascript.dtoa; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +public class DecimalFormatter { + private static final double MAX_FIXED = 1E21; + + /** + * The algorithm of Number.prototype.toExponential. If fractionDigits is < 0, then it indicates + * the special case that the value was previously undefined, which calls for a different + * precision for the calculation. + */ + public static String toExponential(double v, int fractionDigits) { + assert Double.isFinite(v); + + if (fractionDigits < 0) { + // In this case, we are supposed to use the "shortest possible representation." + // This is what our usual toString implementation does + return DoubleFormatter.toDecimal(v).toString(Decimal.Mode.TO_EXPONENTIAL); + } + + boolean negative = Math.signum(v) < 0; + double val = v; + if (negative) { + val = Math.abs(v); + } + BigDecimal bd = + new BigDecimal(val, new MathContext(fractionDigits + 1, RoundingMode.HALF_UP)); + + int exponent; + if (bd.scale() >= 0) { + exponent = bd.precision() - bd.scale() - 1; + } else { + // digits000 + exponent = bd.precision() + -bd.scale() - 1; + } + + return toExponentialString(bd, exponent, fractionDigits, negative); + } + + /** The algorithm of Number.prototype.toFixed(fractionDigits). */ + public static String toFixed(double v, int fractionDigits) { + assert Double.isFinite(v); + assert fractionDigits >= 0; + boolean negative = Math.signum(v) < 0; + double val = v; + if (negative) { + val = Math.abs(v); + } + if (val >= MAX_FIXED) { + return DoubleFormatter.toString(v); + } + BigDecimal bd = new BigDecimal(val, MathContext.UNLIMITED); + if (bd.scale() > fractionDigits) { + bd = bd.setScale(fractionDigits, RoundingMode.HALF_UP); + } + return toFixedString(bd, fractionDigits, negative); + } + + /** The algorithm of Number.prototype.toPrecision() */ + public static String toPrecision(double v, int precision) { + assert Double.isFinite(v); + assert precision >= 1; + boolean negative = Math.signum(v) < 0; + double val; + if (negative) { + val = -v; + } else { + val = v; + } + BigDecimal bd = new BigDecimal(val, new MathContext(precision, RoundingMode.HALF_UP)); + + int scale = bd.scale(); + int numDigits = bd.precision(); + int exponent; + int fractionDigits; + if (scale >= 0) { + if (scale >= numDigits) { + // 0.digits000 + fractionDigits = precision; + } else { + // dig.its + fractionDigits = precision - (numDigits - scale); + } + exponent = numDigits - scale - 1; + } else { + // digits000 + fractionDigits = 0; + exponent = numDigits + -scale - 1; + } + + if (exponent < -6 || exponent >= precision) { + return toExponentialString(bd, exponent, precision - 1, negative); + } + return toFixedString(bd, fractionDigits, negative); + } + + private static String toFixedString(BigDecimal d, int fractionDigits, boolean negative) { + int scale = d.scale(); + // Turns out that, in UNLIMITED scale mode, BigDecimal will not + // produce a negative scale. + assert (scale >= 0); + String digits = d.unscaledValue().toString(); + int numDigits = digits.length(); + if (scale == 0 && fractionDigits == 0) { + if (negative) { + return "-" + digits; + } + return digits; + } + + // Room for digits, -, ., extra 0 + StringBuilder b = new StringBuilder(numDigits * 2 + 3); + if (negative) { + b.append('-'); + } + if (scale >= numDigits) { + // 0.000digits000 + b.append("0."); + fillZeroes(b, scale - numDigits); + b.append(digits); + } else { + // dig.its000 + b.append(digits.substring(0, numDigits - scale)); + b.append('.'); + b.append(digits.substring(numDigits - scale)); + } + fillZeroes(b, fractionDigits - scale); + return b.toString(); + } + + private static String toExponentialString( + BigDecimal d, int exponent, int fractionDigits, boolean negative) { + String digits = d.unscaledValue().toString(); + int numDigits = digits.length(); + // Room for digits, ., -, e+000 + StringBuilder b = new StringBuilder(numDigits + fractionDigits + 7); + + if (negative) { + b.append('-'); + } + b.append(digits.charAt(0)); + if (numDigits > 1 || fractionDigits >= 1) { + b.append('.'); + b.append(digits.substring(1)); + fillZeroes(b, fractionDigits - (numDigits - 1)); + } + b.append('e'); + if (exponent >= 0) { + b.append('+'); + } + b.append(exponent); + return b.toString(); + } + + private static void fillZeroes(StringBuilder b, int count) { + for (int i = 0; i < count; i++) { + b.append('0'); + } + } +}
src/org/mozilla/javascript/dtoa/Decimal.java+22 −11 modified@@ -34,6 +34,11 @@ public class Decimal { private int length; private final char[] buf = new char[MAX_CHARS]; + enum Mode { + DEFAULT, + TO_EXPONENTIAL + }; + Decimal(long d, int e, boolean n) { this.digits = d; this.exponent = e; @@ -46,6 +51,10 @@ public class Decimal { */ @Override public String toString() { + return toString(Mode.DEFAULT); + } + + String toString(Mode mode) { length = 0; /* @@ -101,17 +110,19 @@ public String toString() { append('-'); } - if (0 < e && e <= 8) { - return toFixed(h, m, l, e); - } - if (8 < e && e <= 16) { - return toFixedBigger(h, m, l, e); - } - if (16 < e && e <= 21) { - return toFixedBiggest(h, m, l, e); - } - if (-6 < e && e <= 0) { - return toFixedSmall(h, m, l, e); + if (mode == Mode.DEFAULT) { + if (0 < e && e <= 8) { + return toFixed(h, m, l, e); + } + if (8 < e && e <= 16) { + return toFixedBigger(h, m, l, e); + } + if (16 < e && e <= 21) { + return toFixedBiggest(h, m, l, e); + } + if (-6 < e && e <= 0) { + return toFixedSmall(h, m, l, e); + } } return toExponential(h, m, l, e); }
src/org/mozilla/javascript/dtoa/DoubleFormatter.java+50 −28 modified@@ -82,6 +82,35 @@ public class DoubleFormatter { * can handle any number including non-finite numbers. */ public static String toString(double v) { + long bits = Double.doubleToRawLongBits(v); + long t = bits & T_MASK; + int bq = (int) (bits >>> (P - 1)) & BQ_MASK; + if (bq < BQ_MASK) { + if (bq == 0 && t == 0) { + return "0"; + } + return toDecimalImpl(bits, t, bq).toString(); + } + if (t != 0) { + return "NaN"; + } + return bits > 0 ? "Infinity" : "-Infinity"; + } + + /** + * Convert a double to a Decimal object that may be output in various ways. Unlike toString, and + * since it always returns a Decimal, it must be used with finite numbers only or the result is + * undetermined. + */ + public static Decimal toDecimal(double v) { + assert Double.isFinite(v); + long bits = Double.doubleToRawLongBits(v); + long t = bits & T_MASK; + int bq = (int) (bits >>> (P - 1)) & BQ_MASK; + return toDecimalImpl(bits, t, bq); + } + + private static Decimal toDecimalImpl(long bits, long t, int bq) { /* For full details see references [2] and [1]. @@ -91,42 +120,35 @@ public static String toString(double v) { either 2^(P-1) <= c < 2^P (normal) or 0 < c < 2^(P-1) and q = Q_MIN (subnormal) */ - long bits = Double.doubleToRawLongBits(v); - long t = bits & T_MASK; - int bq = (int) (bits >>> (P - 1)) & BQ_MASK; + // Only finite numbers are supported + assert bq < BQ_MASK; boolean negative = false; - if (bq < BQ_MASK) { - if (bits < 0) { - negative = true; - } - if (bq != 0) { - // normal value. Here mq = -q - int mq = -Q_MIN + 1 - bq; - long c = C_MIN | t; - // The fast path discussed in section 8.2 of [1]. - if (0 < mq && mq < P) { - long f = c >> mq; - if (f << mq == c) { - return new Decimal(f, 0, negative).toString(); - } + if (bits < 0) { + negative = true; + } + if (bq != 0) { + // normal value. Here mq = -q + int mq = -Q_MIN + 1 - bq; + long c = C_MIN | t; + // The fast path discussed in section 8.2 of [1]. + if (0 < mq && mq < P) { + long f = c >> mq; + if (f << mq == c) { + return new Decimal(f, 0, negative); } - return toDecimalImpl(-mq, c, 0, negative).toString(); } - if (t != 0) { - // subnormal value - return t < C_TINY - ? toDecimalImpl(Q_MIN, 10 * t, -1, negative).toString() - : toDecimalImpl(Q_MIN, t, 0, negative).toString(); - } - return "0"; + return toDecimalFull(-mq, c, 0, negative); } if (t != 0) { - return "NaN"; + // subnormal value + return t < C_TINY + ? toDecimalFull(Q_MIN, 10 * t, -1, negative) + : toDecimalFull(Q_MIN, t, 0, negative); } - return bits > 0 ? "Infinity" : "-Infinity"; + return new Decimal(0, 1, false); } - private static Decimal toDecimalImpl(int q, long c, int dk, boolean negative) { + private static Decimal toDecimalFull(int q, long c, int dk, boolean negative) { /* The skeleton corresponds to figure 4 of [1]. The efficient computations are those summarized in figure 7.
src/org/mozilla/javascript/DToA.java+3 −0 modified@@ -354,6 +354,7 @@ static String JS_dtobasestr(int base, double d) return buffer.toString(); } +<<<<<<< HEAD /* dtoa for IEEE arithmetic (dmg): convert double to ASCII string. * @@ -1222,5 +1223,7 @@ else if (i < 4) { } } +======= +>>>>>>> b333c3ec7 (Replace number formatting operations on Number) }
src/org/mozilla/javascript/NativeNumber.java+71 −67 modified@@ -6,6 +6,8 @@ package org.mozilla.javascript; +import org.mozilla.javascript.dtoa.DecimalFormatter; + /** * This class implements the Number native object. * @@ -156,49 +158,13 @@ public Object execIdCall( return ScriptRuntime.wrapNumber(value); case Id_toFixed: - int precisionMin = cx.version < Context.VERSION_ES6 ? -20 : 0; - return num_to(value, args, DToA.DTOSTR_FIXED, DToA.DTOSTR_FIXED, precisionMin, 0); + return js_toFixed(cx, value, args); case Id_toExponential: - { - // Handle special values before range check - if (Double.isNaN(value)) { - return "NaN"; - } - if (Double.isInfinite(value)) { - if (value >= 0) { - return "Infinity"; - } - return "-Infinity"; - } - // General case - return num_to( - value, - args, - DToA.DTOSTR_STANDARD_EXPONENTIAL, - DToA.DTOSTR_EXPONENTIAL, - 0, - 1); - } + return js_toExponential(value, args); case Id_toPrecision: - { - // Undefined precision, fall back to ToString() - if (args.length == 0 || Undefined.isUndefined(args[0])) { - return ScriptRuntime.numberToString(value, 10); - } - // Handle special values before range check - if (Double.isNaN(value)) { - return "NaN"; - } - if (Double.isInfinite(value)) { - if (value >= 0) { - return "Infinity"; - } - return "-Infinity"; - } - return num_to(value, args, DToA.DTOSTR_STANDARD, DToA.DTOSTR_PRECISION, 1, 0); - } + return js_toPrecision(value, args); default: throw new IllegalArgumentException(String.valueOf(id)); @@ -249,37 +215,75 @@ private static Object execConstructorCall(int id, Object[] args) { } } - @Override - public String toString() { - return ScriptRuntime.numberToString(doubleValue, 10); + private static Object js_toFixed(Context cx, double value, Object[] args) { + int fractionDigits; + if (args.length > 0 && !Undefined.isUndefined(args[0])) { + double p = ScriptRuntime.toInteger(args[0]); + int precisionMin = cx.version < Context.VERSION_ES6 ? -20 : 0; + /* We allow a larger range of precision than + ECMA requires; this is permitted by ECMA. */ + checkPrecision(p, precisionMin, args[0]); + fractionDigits = ScriptRuntime.toInt32(p); + } else { + fractionDigits = 0; + } + + if (!Double.isFinite(value)) { + return ScriptRuntime.toString(value); + } + return DecimalFormatter.toFixed(value, fractionDigits); } - private static String num_to( - double val, - Object[] args, - int zeroArgMode, - int oneArgMode, - int precisionMin, - int precisionOffset) { - int precision; - if (args.length == 0) { - precision = 0; - oneArgMode = zeroArgMode; + private static Object js_toExponential(double value, Object[] args) { + double p; + boolean wasUndefined; + if (args.length > 0 && !Undefined.isUndefined(args[0])) { + wasUndefined = false; + p = ScriptRuntime.toInteger(args[0]); } else { - /* We allow a larger range of precision than - ECMA requires; this is permitted by ECMA. */ - double p = ScriptRuntime.toInteger(args[0]); - if (p < precisionMin || p > MAX_PRECISION) { - String msg = - ScriptRuntime.getMessageById( - "msg.bad.precision", ScriptRuntime.toString(args[0])); - throw ScriptRuntime.rangeError(msg); - } - precision = ScriptRuntime.toInt32(p); + wasUndefined = true; + p = 0.0; } - StringBuilder sb = new StringBuilder(); - DToA.JS_dtostr(sb, oneArgMode, precision + precisionOffset, val); - return sb.toString(); + + if (!Double.isFinite(value)) { + return ScriptRuntime.toString(value); + } + checkPrecision(p, 0.0, args.length > 0 ? args[0] : Undefined.instance); + + // Trigger the special handling for undefined, which requires that + // we hold off on this bit until the checks above,. + int fractionDigits = wasUndefined ? -1 : ScriptRuntime.toInt32(p); + + return DecimalFormatter.toExponential(value, fractionDigits); + } + + private static Object js_toPrecision(double value, Object[] args) { + // Undefined precision, fall back to ToString() + if (args.length == 0 || Undefined.isUndefined(args[0])) { + return ScriptRuntime.toString(value); + } + + double p = ScriptRuntime.toInteger(args[0]); + if (!Double.isFinite(value)) { + return ScriptRuntime.toString(value); + } + checkPrecision(p, 1.0, args[0]); + int precision = ScriptRuntime.toInt32(p); + + return DecimalFormatter.toPrecision(value, precision); + } + + private static void checkPrecision(double p, double min, Object arg) { + if (p < min || p > MAX_PRECISION) { + String msg = + ScriptRuntime.getMessageById("msg.bad.precision", ScriptRuntime.toString(arg)); + throw ScriptRuntime.rangeError(msg); + } + } + + @Override + public String toString() { + return ScriptRuntime.numberToString(doubleValue, 10); } static Object isFinite(Object val) { @@ -309,7 +313,7 @@ private static boolean isDoubleInteger(Double d) { } private static boolean isDoubleInteger(double d) { - return !Double.isInfinite(d) && !Double.isNaN(d) && (Math.floor(d) == d); + return Double.isFinite(d) && (Math.floor(d) == d); } private static boolean isSafeInteger(Number val) {
testsrc/test262.properties+1 −5 modified@@ -803,12 +803,8 @@ built-ins/NativeErrors 35/108 (32.41%) URIError/proto-from-ctor-realm.js {unsupported: [Reflect, cross-realm]} built-ins/Number 9/283 (3.18%) - prototype/toExponential/return-abrupt-tointeger-fractiondigits.js - prototype/toExponential/return-abrupt-tointeger-fractiondigits-symbol.js - prototype/toExponential/undefined-fractiondigits.js prototype/toLocaleString/length.js - prototype/toPrecision/nan.js - proto-from-ctor-realm.js {unsupported: [Reflect, cross-realm]} + proto-from-ctor-realm.js {unsupported: [Reflect]} S9.3.1_A2_U180E.js {unsupported: [u180e]} S9.3.1_A3_T1_U180E.js {unsupported: [u180e]} S9.3.1_A3_T2_U180E.js {unsupported: [u180e]}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.