Joda Money — Spec vs Reality

Spec source: README.md
Scope: Source code only — infrastructure config files (Docker, CI/CD, YAML) not analysed
Coverage: ██████████ 8/8 features fully implemented


Feature Coverage

Feature Spec Intent Code Reality Status
Money parsing Create a monetary value using parse, e.g., Money.parse("USD 23.87") Money.parse(String) exists as a factory method that parses a string of the form 'CCC amount' where CCC is a three-let... ✅ Implemented
Money creation from double Create a Money from CurrencyUnit and double, e.g., Money.of(usd, 12.43d) Money.of(CurrencyUnit, double) exists and converts the double via BigDecimal.valueOf before calling of(CurrencyUnit, ... ✅ Implemented
Money addition Add another amount, e.g., money.plus(Money.of(usd, 12.43d)) Money.plus(Money) and other plus methods exist, performing addition with currency validation. ✅ Implemented
Money subtraction of major units Subtract an amount in dollars (major units), e.g., money.minusMajor(2) Money.minusMajor(long) exists and subtracts the specified major units. ✅ Implemented
Money multiplication with rounding Multiply by a factor with rounding, e.g., money.multipliedBy(3.5d, RoundingMode.DOWN) Money.multipliedBy(double, RoundingMode) exists, performing multiplication with specified rounding. ✅ Implemented
Money comparison Compare two amounts, e.g., money.isGreaterThan(dailyWage) Money.isGreaterThan(Money) and other comparison methods exist, delegating to compareTo. ✅ Implemented
Money currency conversion Convert to another currency using a supplied rate, e.g., money.convertedTo(CurrencyUnit.GBP, conversionRate, Rounding... Money.convertedTo(CurrencyUnit, BigDecimal, RoundingMode) exists, converting the amount using the provided rate. ✅ Implemented
BigMoney conversion Use BigMoney for more complex calculations, e.g., money.toBigMoney() Money.toBigMoney() exists and returns the underlying BigMoney instance. ✅ Implemented

What the code does that the spec never mentioned

These behaviors exist in the codebase but have no entry in any spec document. They represent implicit engineering decisions — security contracts, undocumented constraints, behavioral choices — that the spec author either assumed, forgot, or decided after writing the spec.

1. Android zero bug workaround

BigMoney.of(CurrencyUnit, double) returns zero for a zero double value to avoid a bug in stripTrailingZeros() on Android versions before v30.

Evidence: BigMoney.java:L262
Why it matters: This platform-specific behavior could cause unexpected rounding behavior differences on Android vs. other platforms.

2. System property data provider

CurrencyUnit static initializer loads a CurrencyUnitDataProvider specified by system property 'org.joda.money.CurrencyUnitDataProvider', defaulting to DefaultCurrencyUnitDataProvider. If a SecurityException occurs, it falls back to default.

Evidence: CurrencyUnit.java:L50
Why it matters: This allows external configuration of currency data, which could be exploited if the system property is set maliciously, potentially loading incorrect currency definitions.

3. Scale constraint on creation

Money.of(CurrencyUnit, BigDecimal) throws ArithmeticException if the scale of the amount exceeds the currency's decimal places, enforcing strict scale validation. An overload with RoundingMode allows rounding.

Evidence: Money.java:L226
Why it matters: This strict validation may surprise users expecting automatic rounding, potentially causing runtime errors if not handled.

4. Multi-attempt signed parsing

SignedPrinterParser.parse attempts to parse using three formatters (positive, zero, negative) and selects the result with the highest parse index, preferring non-error contexts.

Evidence: SignedPrinterParser.java:L60
Why it matters: This parsing strategy can lead to unexpected results when multiple formatters succeed, potentially selecting a wrong amount if formatting is ambiguous.


Security Observations

Behaviors with security implications that are not documented in the spec. These are not CVEs — they are undocumented security contracts, auth edge cases, and trust-boundary decisions found directly in the code.

🔴 Provider loading via system property HIGH

CurrencyUnit static initializer loads a data provider specified by system property 'org.joda.money.CurrencyUnitDataProvider'. An attacker with access to system properties could supply a malicious provider to inject false currency data.

Evidence: CurrencyUnit.java:L50
Risk: If an attacker can set this system property (e.g., via -D flag or environment), they could load arbitrary currency definitions. This could lead to incorrect conversions, validation bypass, or denial of service.

🟡 Classpath resource loading MEDIUM

DefaultCurrencyUnitDataProvider.registerCurrencies() calls loadFromFiles(), which reads all resources with a given name from the classpath using ClassLoader.getResources(). If an attacker can manipulate the classpath, they could supply malicious CSV files.

Evidence: DefaultCurrencyUnitDataProvider.java:L59
Risk: An attacker controlling the classpath could inject fake currency data, leading to incorrect monetary behavior across the application.

🟢 No input sanitization in parse LOW

The parse method in AmountPrinterParser reads characters from the text and converts localized digits and signs into a BigDecimal. There is no explicit validation of the string format beyond the parsing logic.

Evidence: AmountPrinterParser.java:L129
Risk: Malicious or malformed input could cause parsing errors or produce unexpected amounts, but the parsing is well-defined and rejects invalid inputs via error index.


Questions for the Engineering Team


Generated by Verifiably — every finding is grounded in file:line evidence from the codebase, not training data.