Exploiting floating-point rounding, signed zero, and stale exchange rates to extract micro-credits per transaction — salami slicing scaled to fintech APIs.
TL;DR
mf-calculation, form_id=0) or reuse a PaymentIntent confirmed at $0.01 to fulfil a $299 order.mulDown/mulUp asymmetry across batch swaps.payment_intent.amount_received against the expected amount before fulfilment.Currency rounding abuse is a business logic vulnerability where attackers extract value by exploiting how an application converts, sums, or rounds monetary amounts. Unlike classic price manipulation — where the attacker bluntly tampers with price=0.01 — rounding abuse is structural: the math itself, or the trust model around the math, leaks value at every transaction. The per-event amount is negligible. The aggregate, at API throughput, is material.
The class spans three CWEs and two OWASP entries. CWE-682 (Incorrect Calculation) captures the pure arithmetic error: a tax on $4.997 at 8% yields $0.39976, but a DECIMAL(10,2) column rounds to $0.40 — that lost $0.00024 has to live somewhere. CWE-1339 (Insufficient Precision or Accuracy of a Real Number) covers the IEEE 754 root cause. OWASP A04:2021 — Insecure Design (renamed A06:2025 — Insecure Design in the OWASP Top 10:2025 refresh) applies to the legacy float-based math libraries still in production. And OWASP BLA3:2025 (Object State Manipulations) — added to the 2025 Business Logic Abuse Top 10 — names the modern variant directly: numeric fields accepted from the client without server-side recomputation. The 2026 CVE wave (MetForm Pro, SureForms, Formidable Forms) is not a float bug. It is BLA3 in production.
Price tampering is fixed by validating a single field against a catalogue. Rounding abuse requires choosing the right numeric type, the right rounding mode, and the right verification chain across the entire payment lifecycle. Both can be present in the same endpoint, but they fail differently and require different defences.
IEEE 754 — the 1985 standard implemented by every modern CPU and every float / double / JavaScript Number — represents fractions in binary. Decimal 0.1 has no exact binary representation. The closest 64-bit double is 0.1000000000000000055511151231257827021181583404541015625. Compounding this across an addition chain produces the canonical demonstration:
>>> 0.1 + 0.1 + 0.1
0.30000000000000004 # not 0.3
>>> 0.1 * 3 == 0.3
False
>>> from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.1") + Decimal("0.1") == Decimal("0.3")
TrueIn a shopping cart loop, a tax aggregator, or a currency exchange engine, these ghost fractions accumulate predictably. An attacker who reverse-engineers the implementation can craft inputs that consistently round in their favour.
Any monetary code path containing float, double, or JavaScript Number is presumed vulnerable until proven otherwise. Currency must be stored as integer cents (or millicents for FX) and arithmetic kept in integer space. DECIMAL(10,2) is acceptable for storage but loses sub-cent precision in intermediate calculations.
The second half of the mechanism is rounding direction. Two conventions dominate:
$0.005 rounds to $0.01, $0.004 rounds to $0.00. Predictable but biased upward.$0.005 rounds to $0.00 (nearest even cent), $0.015 rounds to $0.02. Eliminates the upward bias when summing millions of values.Banker's rounding is mathematically superior for unbiased aggregation, but most consumer-facing flows assume half-up. Mixing both inside a single transaction — for example, line items rounded half-up, the cart total rounded banker's — produces the asymmetry that attackers exploit. Pick one mode per domain, document it, and enforce it through a shared utility.
The Balancer V2 exploit of November 2025 is the textbook example: the upscaling path used mulDown (truncation), while the invariant solver computed inputs against the truncated output. The protocol charged slightly less than it should have per swap. Chained inside one batchSwap transaction across thousands of pool operations, the discrepancy compounded into a $128M drainable reserve.
| Variant | Technique | Impact |
|---|---|---|
| Salami slicing classique | Issue millions of micro-transactions, each extracting sub-cent gains via floor/truncate rounding (interest, payroll, micro-deposits) | Quadratic scaling — €23,000/day at 100 req/s on minimum FX denomination |
| Currency arbitrage (stale rates) | Select a low-cost currency at a fixed/cached exchange rate that diverged from spot; pay systematically less in USD-equivalent | HackerOne #1677155 PortSwigger — informational severity, real financial loss when scaled |
| Client-controlled calculation field | Server reads mf-calculation, total, amount from request body and forwards to Stripe without recomputing from product catalogue | CVE-2026-1782 MetForm Pro — any product purchasable for $0.01 unauthenticated |
Signed-zero bypass (quant=-0) | IEEE 754 defines -0.0 == +0.0 but multiplication can produce negative results; total reduces to exactly zero | CVE-2024-50968 — checkout for free, no auth required |
| PaymentIntent reuse | Confirm a $0.01 PaymentIntent, reuse the token in a different higher-value checkout; server validates status=succeeded only | CVE-2026-2890 Formidable Forms — bypass of any payment form on vulnerable WordPress installs |
Boundary form_id=0 | Server derives payment validation from form_id; setting it to a sentinel value bypasses amount validation entirely | CVE-2026-4987 SureForms — Stripe intent created for any attacker-controlled amount |
The Sylius PayPalPlugin (versions < 1.6.1, < 1.7.1, < 2.0.1, patched March 2025) transmits the cart total to PayPal at checkout initiation. If the user adds line items after the PayPal intent is created, PayPal captures only the original (lower) amount, while Sylius marks the order as fully paid against the modified total. CVSS 6.5, vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N. A checkout state synchronisation flaw — the server accepts a financial outcome without revalidating the authoritative source. Source: GitHub Advisory GHSA-pqq3-q84h-pj6x.
quant=-0)itsourcecode Agri-Trading Online Shopping System 1.0 fails to validate the quant parameter in the Add to Cart endpoint. Submitting quant=-0 causes the price calculation price * qty to produce zero. The attacker checks out at no cost. CVSS 7.5. The bug exploits the IEEE 754 quirk that -0.0 == +0.0 is true but (-0.0) * x returns -0.0, which naive aggregation treats as zero. A guard if quant <= 0 or quant != int(quant): reject() blocks the attack. Source: NVD CVE-2024-50968.
MetForm Pro for WordPress (≤ 3.9.7, April 2026) trusts the mf-calculation field in REST form submissions directly as the Stripe/PayPal charge amount, with no server-side recomputation against configured pricing. An unauthenticated attacker submits "mf-calculation": "0.01" and buys any product at that price. CVSS 5.3, CWE-20. The canonical 2026 client-controlled calculation pattern and dominant variant of OWASP BLA3:2025 in the wild. Source: cvefeed.io CVE-2026-1782.
form_id=0 Boundary BypassSureForms for WordPress (≤ 2.5.2, April 2026) derives payment validation in create_payment_intent() from the user-supplied form_id. Setting form_id=0 bypasses configured amount validation entirely; the server creates a Stripe PaymentIntent for an attacker-controlled amount without consulting form configuration. CVSS 7.5, unauthenticated. Source: SentinelOne CVE-2026-4987.
A disclosed report against PortSwigger's commerce platform showed that fixed multi-currency pricing creates arbitrage windows when real-world exchange rates drift from stored rates. A buyer choosing EUR at a stale rate paid substantially less in USD-equivalent than the listed USD price. Informational severity, $0 bounty per program policy, but reportable across any platform offering currency selection without per-transaction rate refresh. Source: HackerOne report #1677155.
Shipt validated cart quantities with integer-only checks but accepted fractional decimals in the request body. Submitting qty=0.5 for a $20 item produced a $10 line item the integer validator missed. Awarded $100 bounty in 2018 — notable because most price-manipulation reports receive $0 (programs scope business logic as informational). The pattern remains dominant: integer validation on a numeric field the math layer treats as a float. Source: HackerOne report #388564.
The Balancer V2 incident of November 2025 ($128M loss, Trail of Bits analysis) and the 2017 itBit Bitcoin exchange disclosure (HackerNews #13784755) extend the pattern from web checkouts to DeFi and centralised exchanges. The mathematics is identical; only the throughput and reserve sizes differ.
Burp Suite Repeater is the primary tool. Map every endpoint accepting a numeric monetary field — /checkout, /cart/add, /exchange, /transfer, /payment-intent, plus any REST form submission carrying amount, total, price, calculation, qty. For each endpoint, run:
0.001, 0.0049, 0.0050, 0.0051. Inconsistency between line-item and cart-total rounding is reportable.quant=-0, amount=-0.0. Any successful $0 checkout is the CVE-2024-50968 pattern.qty=0.5 for an integer-priced item (Shipt #388564 pattern).calculation, total, mf-calculation, final_price with 0.01; observe whether the server recomputes from the catalogue (CVE-2026-1782 pattern).payment_intent_id, substitute it in a higher-value checkout. If amount_received is not validated, fulfilment proceeds (CVE-2026-2890 pattern).0.01 USD → EUR → USD repeatedly; net positive balance is the ACROS Security 2012 pattern (€23,000/day documented).form_id — for WordPress payment plugins, submit form_id=0, form_id=-1 (CVE-2026-4987 pattern).Static analysis catches the structural problem: floating-point types in monetary contexts. A Semgrep rule:
rules:
- id: float-in-monetary-context
patterns:
- pattern-either:
- pattern: float $PRICE = ...
- pattern: double $AMOUNT = ...
- pattern: Number($PRICE)
message: "Floating-point type used for monetary value — use BigDecimal, decimal.Decimal, or integer cents (CWE-1339)"
languages: [java, kotlin, python, javascript, typescript]
severity: WARNING
metadata:
cwe: CWE-1339
owasp: BLA3:2025CodeQL adds dataflow reasoning: trace float/double variables that flow into payment provider sinks (Stripe amount, PayPal total). Per Rafter's 2026 SAST comparison, CodeQL outperforms pattern matchers in this category because it traces the calculation chain end-to-end. For Python, pylint with a custom checker flags float() literals where Decimal is expected, and mypy strict mode enforces a Money type domain across the codebase.
DAST scanners alone cannot detect this class. Burp Active Scan and OWASP ZAP do not understand which fields represent money or what the expected charge should be — they have no oracle for "this user paid $0.01 for a $299 product." Detection requires source-code review or a stateful pentest harness that knows the catalogue ground truth.
BreachVex detects this via typed numeric fuzzing on amount, quantity, and total fields including signed-zero injection (-0, -0.0), IEEE 754 boundary values, client-calculation field substitution against a baseline catalogue snapshot, and PaymentIntent reuse across distinct order flows. Findings are confirmed when the differential between the submitted and expected charge is observed in a verified transaction response.
The canonical rule: store money as integers (cents, satoshis, millicents) and compute in integer space. Round once, at the presentation layer. When integers are not practical, use decimal.Decimal (Python) or BigDecimal (Java) with explicit rounding mode.
# WRONG — never do this
price = 0.1 + 0.2 # 0.30000000000000004
tax = price * 0.08 # 0.024000000000000004
total = round(price + tax, 2)
# Drift compounds across thousands of orders.
# CORRECT — Decimal with explicit rounding
from decimal import Decimal, ROUND_HALF_EVEN
CENT = Decimal("0.01")
def compute_total(unit_price_cents: int, qty: int, tax_bps: int) -> int:
"""All integer arithmetic; no float involved."""
subtotal_cents = unit_price_cents * qty
tax_cents = (subtotal_cents * tax_bps + 5000) // 10000 # half-up
return subtotal_cents + tax_cents
# OR, when integers are impractical (FX, percentages):
def round_currency(amount: Decimal) -> Decimal:
# Banker's rounding — unbiased for large aggregates
return amount.quantize(CENT, rounding=ROUND_HALF_EVEN)The Java equivalent uses BigDecimal with the String constructor — never new BigDecimal(0.08), which inherits the float imprecision (0.08000000000000000166533...). Use new BigDecimal("0.08") or BigDecimal.valueOf(8, 2). Apply setScale(2, RoundingMode.HALF_UP) only at the final emission point.
Database schemas matter: amount FLOAT is an immediate finding, amount DECIMAL(10,2) is acceptable for storage but loses sub-cent precision when intermediate calculations leave the database. The Modern Treasury engineering team stores all USD as int64 (PostgreSQL bigint) precisely to eliminate this drift.
The 2025–2026 CVE wave exploits trust in client-supplied amounts and stale payment references. Two controls close the gap:
// Node.js / Stripe — verify intent amount before fulfilment
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
async function fulfilOrder(orderId, paymentIntentId) {
// 1. Recompute expected charge from the authoritative catalogue
const order = await db.order.findUnique({
where: { id: orderId },
include: { items: { include: { product: true } } },
});
const expectedCents = order.items.reduce(
(sum, item) => sum + item.product.priceCents * item.qty,
0,
);
// 2. Bind the intent to this specific order — reject reuse
if (order.paymentIntentId !== paymentIntentId) {
throw new Error('PaymentIntent does not match order');
}
// 3. Retrieve from Stripe — never trust client-supplied status
const intent = await stripe.paymentIntents.retrieve(paymentIntentId);
// 4. Validate amount AND status
if (intent.status !== 'succeeded') {
throw new Error(`Intent status ${intent.status}, expected succeeded`);
}
if (intent.amount_received !== expectedCents) {
throw new Error(
`Amount mismatch: received ${intent.amount_received}, expected ${expectedCents}`,
);
}
// 5. Idempotent transition to paid
await db.order.update({
where: { id: orderId, status: 'pending_payment' },
data: { status: 'paid', paidAt: new Date() },
});
}Five mandatory hardening controls:
amount_received == expected_amount at fulfilment via the provider API.form_id=0 and boundary values in endpoints that derive pricing from form configuration.For currency arbitrage, fetch rates from an authoritative source at checkout, bind the rate to the order record, and refuse quotes older than a short TTL (60–300 seconds). Apply rate limiting on FX endpoints.
-0) attacks share the regex evasion pattern.Currency rounding abuse is a business logic flaw where attackers extract value by exploiting how an application converts, sums, or rounds monetary amounts. It maps to CWE-682 (Incorrect Calculation), CWE-1339 (Insufficient Numeric Precision), and OWASP BLA3:2025 Object State Manipulations. Modern variants include CVE-2026-1782 (MetForm Pro client-supplied `mf-calculation`) and the Balancer V2 incident of November 2025 ($128M drained via `mulDown`/`mulUp` asymmetry), proving the class scales from web checkouts to DeFi reserves.
Salami slicing is the practice of issuing millions of micro-transactions, each extracting a sub-cent amount via floor or truncate rounding, so the per-event loss escapes detection while the aggregate is material. The 2012 ACROS Security disclosure documented €23,000 per day extracted at roughly 100 requests per second against an EU online bank using the minimum FX denomination. Modern equivalents target interest accruals, payroll deductions, and cryptocurrency micro-deposits where rounding direction is non-uniform.
JavaScript's `Number` type is an IEEE 754 64-bit double, which represents fractions in binary. Decimal `0.1` has no exact binary representation — the closest double is `0.1000000000000000055511151231257827021181583404541015625`. Summing three of these yields `0.30000000000000004`, not `0.3`. The same behaviour exists in Python `float`, Java `double`, and every modern CPU's FPU. For monetary code, use integer cents, `decimal.Decimal` (Python), or `BigDecimal` (Java) constructed from a string — never from a float literal.
OWASP BLA3:2025 (Object State Manipulations) is the third entry in the OWASP Top 10 for Business Logic Abuse, published 2025. It names the modern rounding-abuse variant directly: numeric fields accepted from the client without server-side recomputation against the authoritative catalogue. The 2026 CVE wave (CVE-2026-1782 MetForm Pro, CVE-2026-2890 Formidable Forms, CVE-2026-4987 SureForms) is BLA3 in production — servers trust `mf-calculation`, `total`, or `form_id=0` and forward the value to Stripe without recomputing the expected charge.
Five mandatory controls. (1) Store money as integer cents and compute in integer space; round only at the presentation layer. (2) Recompute every charge server-side from the product catalogue — never trust client-supplied `amount`, `total`, or `mf-calculation` fields. (3) Validate `payment_intent.amount_received == expected_amount` before fulfilment via the provider API. (4) Bind PaymentIntents to order IDs at creation and reject reuse across orders (CVE-2026-2890 pattern). (5) Reject boundary values like `form_id=0`, `qty=-0`, and fractional quantities on integer-priced items (HackerOne #388564, CVE-2024-50968).
IEEE 754 is the 1985 binary floating-point standard implemented by every CPU's FPU — `float`, `double`, and JavaScript `Number` are all IEEE 754. It stores fractions in base 2, so `0.1` is inexact, and arithmetic accumulates rounding error. `Decimal` (Python `decimal.Decimal`, Java `BigDecimal`, .NET `decimal`) stores numbers in base 10 with arbitrary precision and explicit rounding modes. For currency, IEEE 754 is unsafe — the Modern Treasury engineering team stores all USD as `int64` precisely because float drift compounds across millions of transactions.
Banker's rounding (round half to even, the IEEE 754 default and GAAP-preferred mode) is mathematically unbiased for large aggregates — `$0.005` rounds to `$0.00`, `$0.015` rounds to `$0.02`. It is not exploitable on its own. The exploit appears when two rounding modes coexist inside one transaction — for example, line items rounded half-up while the cart total uses banker's rounding. This asymmetry is exactly the Balancer V2 pattern (`mulDown` upscaling against an invariant solver expecting symmetric rounding) that drained $128M in November 2025. Pick one mode per domain, document it, and enforce it through a shared utility.