Client Supplied Price
ID |
client_supplied_price |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Family |
API6:2023 - Unrestricted Access to Sensitive Business Flows |
CWE |
CWE-472, CWE-602 |
Resource |
source |
Language |
JavaScript/TypeScript, Python, Java, Kotlin, Go, C#, PHP, Ruby, Swift |
Description
A price or a total the client sends is saved, or charged, as it was sent. The server had what it needed to compute it and took the client’s word instead: the client chooses what it pays.
The detector follows the endpoint’s handler into the services and helpers it calls, and reports a
value of the request named as a price or a total (the last word of its name is on derivedNames:
price, total, subtotal, cost, fee…) that reaches a money field — of an order, an invoice,
a basket, or the user’s own credit — or a call that moves money, without being computed from the
database or compared with it. Comparing it with the balance it is debited from
(if (account.balance < price)) checks the funds, not the price.
| Severity | When |
|---|---|
high |
The handler loaded the record the price belongs to by a value of the same request — the product by its id, the coupon by its code — and saved the client’s figure instead of the record’s. Any client value is reported then, a bare |
low |
A client price or total saved with no record at hand. |
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
An attacker edits the request and buys at the price they choose, redeems a coupon for more than it is worth, or credits their own account.
Example
router.post('/orders', async (req, res) => {
const { productId, price } = req.body;
const product = await Product.findByPk(productId);
await Order.create({ userId: req.user.id, productId: product.id, total: price }); // the client's price
res.sendStatus(201);
});
Fixed:
router.post('/orders', async (req, res) => {
const { productId } = req.body;
const product = await Product.findByPk(productId);
await Order.create({ userId: req.user.id, productId: product.id, total: product.price });
res.sendStatus(201);
});
Remediation
-
Compute every price and total on the server from what it stores, and ignore the one in the body.
-
If the client must send it, compare it with the server’s figure and refuse the request when they differ.
Not Reported
-
A value compared with one read from the database (
if (price !== product.price) return …). -
A bare amount with no record loaded, such as a wallet top-up: there was nothing the server could have computed it from.
unbounded_amountreports one with no lower bound. -
A catalogue’s own price set by the client: it reaches no order.
-
A value that goes through a callable the convention profile names in
priceAuthorities(PricingService#settle): the server set it. It is recorded at info, with the fact, so that it can be checked. -
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 onsignatureVerifications): the provider signed the figure, and the client cannot change it.
Coverage
| Language | Covered |
|---|---|
JavaScript and TypeScript |
Express, NestJS; Sequelize, TypeORM, Prisma, Mongoose, knex; a unique key declared only in the SQL schema; route, router and application middleware ( |
Python |
Django and DRF (serializers, forms, generic views and ViewSets), Flask (Flask-WTF, marshmallow), FastAPI and Pydantic; the Django ORM, SQLAlchemy, Peewee, Tortoise and SQL run on a cursor; a price compared with the product’s in a helper; Stripe (also a |
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
derivedNames lists the names of the values a server computes.
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.