CWE-197: Numeric Truncation Error - Java
Overview
Java's narrowing primitive conversions (long to int, double to float, double/float to an integer type) are silent - the compiler requires an explicit cast, but the cast itself performs no range check. A long value outside int's range loses its high-order bits when cast to int. A double-to-int cast is defined behavior in Java (unlike C's undefined behavior for out-of-range conversions), but "defined" here means it clamps to Integer.MAX_VALUE/Integer.MIN_VALUE or produces 0 for NaN. That is not an error, so out-of-range input still silently produces a value the caller didn't expect. float and double also lose precision on ordinary arithmetic, which matters for financial calculations.
Common Vulnerable Patterns
Unchecked long to int Narrowing
public class FileHandler {
// VULNERABLE - long-to-int cast with no range check
public static byte[] allocateFromFileSize(long fileSize) {
int bufferSize = (int) fileSize; // truncates if fileSize > Integer.MAX_VALUE
return new byte[bufferSize]; // wrong-sized (or negative-length -> exception) array
}
}
Why this is vulnerable: long can represent values far beyond Integer.MAX_VALUE (about 2.1 billion). A fileSize above that truncates to an unrelated int, which can even come out negative - new byte[] with a negative length throws NegativeArraySizeException, but a truncated positive value that's simply wrong causes a silently undersized allocation instead.
Unchecked double to int Narrowing
public class Calculator {
// VULNERABLE - double-to-int cast with no range check
public static int computeResult(double userValue) {
// Java clamps out-of-range doubles rather than being undefined,
// but the clamp is still the wrong value for the caller's intent
return (int) userValue;
}
}
Why this is vulnerable: For userValue above Integer.MAX_VALUE, the cast clamps to Integer.MAX_VALUE rather than throwing - code that expects the cast to fail loudly on out-of-range input instead gets a plausible-looking but wrong number.
float for Financial Calculations
public class Pricing {
// VULNERABLE - float lacks the precision needed for money
public static float calculateTotal(float price, float quantity) {
return price * quantity; // float has ~7 significant decimal digits
}
}
Why this is vulnerable: float's precision is too coarse for money - repeated arithmetic on float amounts accumulates rounding error that shows up as cents (or more, at scale) silently going missing or appearing.
Secure Patterns
Validated long to int Conversion
public class FileHandler {
public static int safeLongToInt(long value) {
if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Value " + value + " cannot fit in int");
}
return (int) value;
}
public static byte[] allocateFromFileSize(long fileSize) {
int bufferSize = safeLongToInt(fileSize);
if (bufferSize <= 0 || bufferSize > 100_000_000) {
throw new IllegalArgumentException("Invalid buffer size: " + bufferSize);
}
return new byte[bufferSize];
}
}
Why this works: Checking the long value's range before casting - or using Math.toIntExact(value), which does the same check and throws ArithmeticException on overflow - means an out-of-range value is rejected instead of silently truncated.
Validated double to int Conversion
public class Calculator {
public static int safeDoubleToInt(double value) {
if (Double.isNaN(value) || Double.isInfinite(value)) {
throw new IllegalArgumentException("Invalid value: " + value);
}
if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Value out of int range: " + value);
}
return (int) value;
}
}
Why this works: Explicitly checking for NaN, infinity, and the int range before casting turns Java's silent clamp-and-continue behavior into an explicit failure the caller must handle.
BigDecimal for Financial Calculations
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Pricing {
public static BigDecimal calculateTotal(BigDecimal price, BigDecimal quantity) {
return price.multiply(quantity).setScale(2, RoundingMode.HALF_UP);
}
}
Why this works: BigDecimal represents decimal values exactly (as an unscaled integer plus a scale), so it has no binary floating-point rounding error at all - eliminating the precision-loss class of truncation bug for money rather than just narrowing its impact.
Framework-Specific Guidance
Bean Validation (Jakarta)
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
public class FileUploadRequest {
@Min(0)
@Max(Integer.MAX_VALUE)
private long declaredSize;
}
Why this works: Applying @Min/@Max to the wide (long) field, before any narrowing conversion happens downstream, lets the framework reject an out-of-range value at the API boundary instead of relying on every internal consumer to re-check it.
Considerations
Decide whether the narrowing is provably safe for this input domain. A conversion is fine when the source is already bounded - validated at an API boundary, or structurally limited - and the question is whether that bound holds for every caller rather than the one in front of you. The cases where it usually does not are file sizes, lengths reported by a remote peer, aggregated sums that grow over time, and timestamps beyond the 32-bit epoch. If you cannot point at where the value was bounded, treat it as unbounded.
Testing
- Unit-test conversion helpers with
Integer.MIN_VALUE,Integer.MAX_VALUE, values one past each boundary, andLong.MAX_VALUE. - For
double/floatsources, explicitly testDouble.NaN,Double.POSITIVE_INFINITY, andDouble.NEGATIVE_INFINITY. - For financial code, add a regression test that sums many small
BigDecimalamounts and asserts the total matches exactly (afloat/doubleequivalent would typically drift). - Property-test any conversion reachable from untrusted input over the full
longrange, asserting that every value outsideint's range raises rather than converting. - Do not read a clean SpotBugs run as evidence the narrowing is checked. Its
ICASTdetectors cover a few specific conversion mistakes -ICAST_INTEGER_MULTIPLY_CAST_TO_LONG,ICAST_IDIV_CAST_TO_DOUBLE- and a plain(int) longValueis not among them.
Common Pitfalls
- Assuming
(int) longValuethrows on overflow: it does not - Java requires the cast to be explicit, but the cast itself is a silent truncation, not a checked operation.Math.toIntExact()is the checked equivalent. - Treating
(int) doubleValueas safe because Java defines it where C leaves it undefined: Java's defined clamping behavior (Integer.MAX_VALUE/MIN_VALUE, or0forNaN) is still a silently wrong value for code that expected the input to be in range. - Switching from
floattodoubleand calling it fixed:doublereduces the rounding error but does not eliminate it - it is still binary floating point and still wrong for exact decimal arithmetic like currency. - Validating the API boundary but not internal recomputation: a value that entered the system validated can still be re-derived (summed, multiplied) internally in a way that exceeds range again before a later narrowing cast.