IDOR in POST /rest/basket/:id/checkout — checkout any basket and bill/credit an arbitrary user's wallet
The /rest/basket/:id/checkout handler trusts both the URL basket id and a client-supplied `req.body.UserId` to charge/credit wallets, so any authenticated user can place orders against another user's basket and debit or credit any wallet they choose.
Route registration in server.ts:
app.post('/rest/basket/:id/checkout', placeOrder())
Only the earlier app.use('/rest/basket/:id', security.isAuthorized()) runs as middleware — note there is no security.appendUserId() on this route, so req.body.UserId is fully attacker-controlled.
routes/order.ts line 34-35:
const id = req.params.id
BasketModel.findOne({ where: { id }, include: [...] })
The basket is looked up purely by URL id with no ownership check.
routes/order.ts lines 142-158:
if (req.body.UserId) {
if (req.body.orderDetails && req.body.orderDetails.paymentId === 'wallet') {
const wallet = await WalletModel.findOne({ where: { UserId: req.body.UserId } })
if ((wallet != null) && wallet.balance >= totalPrice) {
await WalletModel.decrement({ balance: totalPrice }, { where: { UserId: req.body.UserId } })
} ...
}
...
await WalletModel.increment({ balance: totalPoints }, { where: { UserId: req.body.UserId } })
}
The wallet that pays / receives bonus points is identified solely by req.body.UserId, never compared against the JWT identity. Combined with the basket IDOR, an attacker can checkout someone else's basket AND drain that (or any other) user's wallet for the payment.
Attacker logged in normally; let basket id 2 belong to a victim with a funded wallet (UserId 2).
POST /rest/basket/2/checkout HTTP/1.1
Host: target
Authorization: Bearer <attacker-JWT>
Content-Type: application/json
{
"UserId": 2,
"orderDetails": { "paymentId": "wallet", "addressId": 1, "deliveryMethodId": 1 }
}
Server fetches basket 2, totals the cart, debits user 2's wallet, generates an order PDF, and then credits user 2's wallet with bonus points (which the attacker can flip by lowering totals, etc.). Crucially neither the basket id nor the wallet UserId is checked against the attacker's JWT.
Authenticated user can: (a) place orders against any other user's basket; (b) drain or credit any user's wallet balance because req.body.UserId is unbounded. Combined with the basket-read IDOR, an attacker can fully impersonate another customer's checkout flow.
The handler uses req.params.id to load the basket via BasketModel.findOne({ where: { id } }) with no check that the basket belongs to the authenticated user, and lines 142–158 use req.body.UserId directly in WalletModel.findOne/decrement/increment without comparing it to the JWT identity from security.authenticatedUsers.from(req). Only security.isAuthorized() runs as middleware on /rest/basket/:id, so any authenticated user can submit another user's basket id together with an arbitrary UserId in the body to debit/credit that wallet. The PoC payload matches the actual code path (wallet payment branch and the unconditional bonus-point increment). Exploit chain is reachable from untrusted input.
The route is reachable over HTTP and only security.isAuthorized() gates it (any logged-in user passes), so AV:N/AC:L/PR:L/UI:N. The handler in routes/order.ts uses req.params.id to fetch any basket with no ownership check and trusts req.body.UserId for both WalletModel.decrement and WalletModel.increment, so an attacker can place orders against another user's cart and arbitrarily debit or credit any wallet — a high integrity impact, plus some confidentiality leak via the basket's product list rendered into the order PDF (C:L). Availability is L because draining a victim's wallet renders their stored funds unusable and decrements product inventory, but the application itself remains up; scope stays Unchanged since all impact is within the Juice Shop app's own authority.
- CWE-639
- CWE-862
- OWASP A01:2021 - Broken Access Control