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 amount included.

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_amount reports 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 on signatureVerifications): 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 (app.use) and NestJS interceptors, global ones included, as the idempotency mechanism; Stripe, PayPal, Adyen, Braintree, Square, Mollie, Checkout.com, Razorpay

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 StripeClient), PayPal (REST SDK and Server SDK), Braintree, Adyen, Square, Mollie, Checkout.com, Razorpay, and the checkouts that price what Stripe, PayPal, Adyen, Square, Mollie, Checkout.com and Razorpay charge on their own page

Java

Spring MVC, JAX-RS and servlets (Bean Validation @Positive, @Min, @DecimalMin, @Range under @Valid or @Validated, constraints of the project, Assert.isTrue, Preconditions.checkArgument) and the repositories Spring Data REST exports (@RepositoryRestResource); Spring Data JPA, MyBatis and MyBatis-Plus, an EntityManager, JDBC and JdbcTemplate, Spring’s JdbcClient, jOOQ, QueryDSL, MongoDB and Redis counters, a stored procedure handed money, and a unique key declared only in the SQL schema; filters, interceptors and controller advice as the idempotency mechanism; Stripe, Braintree, PayPal, Adyen, Square, Mollie, Checkout.com, Razorpay

Kotlin

Spring MVC (@RequestParam, @PathVariable, @RequestBody, @RequestHeader, a simple parameter with no annotation, the servlet request; Bean Validation @Positive, @Min, @DecimalMin, also as @field:, and constraints of the project composed of them; require, check, Assert.isTrue, Preconditions.checkArgument, when entries) and Ktor (call.receive<T>(), receiveParameters(), call.parameters, getOrFail); Spring Data JPA repositories (save, saveAll, apply/also blocks, @Modifying @Query), JdbcTemplate, and Exposed (insert, update, it.update(column, …), new, uniqueIndex()), and a unique key declared only in the SQL schema or a duplicate key caught after the insert; filters, interceptors, controller advice and Ktor plugins as the idempotency mechanism; Stripe, Braintree, PayPal, Adyen, Square, Mollie, Checkout.com, Razorpay

Go

net/http, Gin, Echo, Fiber, with the request’s body decoded into a struct (ShouldBindJSON, Bind, json.NewDecoder(r.Body).Decode, BodyParser); GORM (Create, Save, Update, Updates, gorm.Expr), ent (SetX/AddX … Save) and database/sql, sqlx or pgx (Exec of an UPDATE … SET or an INSERT); binding and validate tags (gt, gte, min), ozzo-validation, unsigned types, math.Abs, max(0, x); gorm:"uniqueIndex", gorm:"unique", ent’s Unique(); route middleware as the idempotency mechanism; stripe-go, braintree-go, PayPal (plutov/paypal), Adyen, Square, Checkout.com, Razorpay

C#

ASP.NET Core controllers ([FromQuery], [FromRoute], [FromBody], [FromHeader], and the binding ASP.NET Core infers with none; [Range] under [ApiController] or a ModelState.IsValid check; FluentValidation’s RuleFor(…).GreaterThan(0), IValidatableObject, validation attributes of the project, Math.Clamp; Request.ReadFromJsonAsync<T>(), Request.Body, JsonDocument) and minimal APIs, registered as a method group or written in line; EF Core (Add/Update on a DbSet or the context, a change to a row a query loaded saved by SaveChanges, ExecuteUpdate with SetProperty, Entry(…).Property(…).CurrentValue, CurrentValues.SetValues, AutoMapper’s Map; [Index(…, IsUnique = true)], HasIndex(…).IsUnique()), linq2db and SQL written by hand (Dapper Execute, ExecuteSqlRaw, ExecuteSqlInterpolated, ADO.NET commands with SqlParameter); filters and middleware as the idempotency mechanism; Stripe.net, Braintree, PayPal, Adyen, Square, Mollie, Checkout.com, Razorpay, and HttpClient calls to Stripe’s and PayPal’s API

PHP

Laravel actions ($request→input(), $request→amount, $request→validated(), $request→validate([…​]), request(), route parameters, lists the body carries; validation rules min:, gt:, gte:, between: in the action or a Form Request, also for each element of a list (items.*.quantity); unique in a migration or the SQL schema) and Symfony actions (the request’s bags, payload and decoded body; [MapRequestPayload] with [Assert\Positive], GreaterThan, Range) on Eloquent (create, update, save, fill, a whole validated body, relations and the authenticated user, increment/decrement, firstOrCreate), the query builder (DB::table(), DB::raw(), SQL with its bindings) and Doctrine (flush() after a setter, persist(), DQL); route, group and controller middleware as the idempotency mechanism; Stripe (also Checkout), PayPal, Adyen, Braintree, Square, Mollie, Checkout.com, Razorpay

Ruby

Rails (params[:amount], params.fetch, strong parameters params.require(…).permit(…), also returned by a private method; request.headers) and Sinatra (params['amount'], request.env['HTTP_…']); ActiveRecord (create, update, save after assigning the row or assign_attributes, increment!/decrement!, update_column, find_or_create_by; validates … numericality: { greater_than: … }, validates … uniqueness: true, unique indexes in db/schema.rb or a migration, also over several columns, and a unique key declared only in the SQL schema (db/structure.sql)); associations (create!, build, <<), params.dig, permit!, hashes built by hand or merged, validations of a superclass or a concern, before_action/around_action filters (also from a concern) as the idempotency mechanism; Stripe, Braintree, PayPal, Adyen, Square, Mollie, Checkout.com, Razorpay, and the checkouts that price what Stripe, Adyen, Square, Mollie, Checkout.com and Razorpay charge on their own page

Swift

Vapor (req.content.decode(T.self), req.content.get(:at:), req.query.get(:at:), req.parameters.get(_:), req.headers.first(name:); Validatable with a lower end, .range(1…​), when the handler calls validate(content:); UInt; guard amount > 0, abs, max(0, …)); Fluent (save/create/update(on:) of a model read or built, query(on:).set(\.$field, to:).update(), .unique(on:) in a migration) and SQL given to SQLKit’s raw; StripeKit

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.

© 2026 Xygeni. All rights reserved