Unbounded Amount
ID |
unbounded_amount |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Family |
API6:2023 - Unrestricted Access to Sensitive Business Flows |
CWE |
CWE-839, CWE-1284 |
Resource |
source |
Language |
JavaScript/TypeScript, Python, Java, Kotlin, Go, C#, PHP, Ruby, Swift |
Description
An amount the client sends reaches a write of money with nothing keeping it at or above zero. A negative top-up debits the wallet it was meant to credit; a negative quantity, multiplied by the price at checkout, turns the charge into a credit.
The detector follows the endpoint’s handler into the services and helpers it calls, and follows each
value of the request — req.body, req.query, req.params, the raw body, a NestJS @Body() DTO —
through locals, destructuring, arithmetic and the calls it is passed to. It reports the value when
it reaches, with no lower bound:
-
a field that holds money: the last word of its name is on the
moneyFieldNameslist of the detector’s configuration (balance,amount,price,total…), or the convention profile names it inmoneyFields; -
a call that moves money: a payment SDK’s (Stripe, PayPal, Adyen, Braintree), or one the profile names in
moneySinks; -
a field the application multiplies by money in another handler: a basket item’s
quantity, saved here and multiplied by the product’s price at checkout.
The generic CRUD of finale-rest saves a model’s fields as the client sends them with no handler of
the application’s own; a model exposed that way whose field is multiplied by money is reported at
the finale.resource(…) call.
When the analysis of your application names the field or the call as money, the finding is reviewed again, and it can end up as info if that review concludes that it does not move money.
Security Impact
The sign of the amount decides which way the money moves. Sending -500 to a top-up endpoint
withdraws instead of depositing; a basket with a quantity of -3 pays the customer at checkout.
Example
router.put('/wallet/balance', async (req, res) => {
const { amount } = req.body;
await Account.increment({ balance: amount }, { where: { userId: req.user.id } }); // a negative top-up debits
res.sendStatus(200);
});
Fixed:
router.put('/wallet/balance', async (req, res) => {
const { amount } = req.body;
if (!(amount > 0)) {
return res.sendStatus(400);
}
await Account.increment({ balance: amount }, { where: { userId: req.user.id } });
res.sendStatus(200);
});
What counts as a lower bound
-
A check that turns a small value away before the write:
if (amount ⇐ 0) return …,if (quantity < 1) throw …. -
A check the write sits behind:
if (amount > 0) { … }. -
A class-validator decorator on the DTO field:
@IsPositive(),@Min(n)withnnot negative. -
A Joi schema the value is validated against, with
.positive(), or.min(n)or.greater(n)for annnot negative:schema.validate(req.body),Joi.attempt(body, schema), or celebrate on the route. -
Math.abs(amount),Math.max(0, amount); in Python,abs(amount)andmax(0, amount). -
For finale-rest, a write hook that refuses the value:
resource.update.write.before(…).
An upper cap (if (amount > LIMIT) return …) is not a lower bound: it lets a negative amount through.
A value handed, before the write, to a call that verifies a payment provider’s signature over it
($this→payU→verifyHash($request→all()), a name on signatureVerifications) is not reported:
the provider signed the figure, and the client cannot change it.
Remediation
-
Reject an amount below the smallest one the operation allows before it reaches the write;
!(amount > 0)also turns a missing or non-numeric amount away. -
Or declare it on the body with class-validator (
@IsPositive(),@Min(1)) and validate it with aValidationPipe. -
For a generic CRUD, add a write hook that refuses it.
Not Reported
-
An amount computed on the server from what the database holds.
-
A catalogue’s own price set by the client (
PUT /products/:id): it is not an amount moved.client_supplied_pricereports a price that reaches an order. -
A field that holds no money and that the application never multiplies by money.
Coverage
| Language | Covered |
|---|---|
JavaScript and TypeScript |
Express, NestJS ( |
Python |
Django and DRF (serializers and forms: |
Java |
Spring MVC, JAX-RS and servlets (Bean Validation |
Kotlin |
Spring MVC ( |
Go |
net/http, Gin, Echo, Fiber, with the request’s body decoded into a struct ( |
C# |
ASP.NET Core controllers ( |
PHP |
Laravel actions ( |
Ruby |
Rails ( |
Swift |
Vapor ( |
Configuration
The detector’s properties list, in its own configuration, the money field names (moneyFieldNames), the
payment SDKs' calls (paymentSdks) and, per language, how a value of the request is followed to a write
(flow: the request members, the ORM calls, the validators, Joi and finale-rest). A key overridden in
the detector’s custom configuration changes it for this detector alone.
The convention profile adds moneyFields and moneySinks.