IDOR in PUT /rest/basket/:id/coupon/:coupon allows applying coupons to any basket
The authenticated apply-coupon endpoint updates the basket identified solely by URL id without verifying the basket belongs to the caller, letting a user attach (or clear) coupons on other users' baskets.
server.ts:
app.put('/rest/basket/:id/coupon/:coupon', utils.asyncHandler(applyCoupon()))
Guarded by app.use('/rest/basket/:id', security.isAuthorized()) for auth only.
routes/coupon.ts:
const id = params.id
let coupon = params.coupon ? decodeURIComponent(params.coupon) : undefined
...
const basket = await BasketModel.findByPk(id)
if (!basket) { ... }
await basket.update({ coupon: coupon?.toString() })
The basket is fetched by findByPk(id) and mutated with no where: { UserId: ... } clause and no check against security.authenticatedUsers.from(req).bid.
PUT /rest/basket/2/coupon/n1y4tlavupr HTTP/1.1
Host: target
Authorization: Bearer <attacker-JWT>
The coupon (a valid coupon code) is now persisted on basket 2 (the victim's basket), even though the attacker is authenticated as a different user. Conversely, sending an invalid coupon null-s out the victim's previously applied coupon.
Any authenticated user can manipulate the coupon field on any basket — granting discounts to others' carts or wiping coupons they had legitimately applied. Trivially exploitable by sequentially trying basket ids.
The applyCoupon handler reads params.id, calls BasketModel.findByPk(id), and then basket.update({ coupon }) with no ownership check against req.user/authenticatedUsers.from(req).bid. The security.isAuthorized() middleware only verifies the JWT but does not bind :id to the caller's basket, so any authenticated user can set/clear the coupon on another user's basket by iterating numeric ids. The exploit chain (authenticated request → arbitrary basket id → persisted mutation) is fully reachable from untrusted input, matching the reported PoC.
The endpoint is reachable over HTTP (AV:N) and only requires any authenticated user thanks to security.isAuthorized() (PR:L); no victim action is needed (UI:N) and exploitation is just iterating basket ids (AC:L). The route calls BasketModel.findByPk(id) then basket.update({ coupon }) without checking ownership against security.authenticatedUsers.from(req).bid, so the attacker can write/clear the coupon field on any basket — a bounded integrity impact limited to one column, hence I:L. No basket contents are returned in the response (C:N) and clearing a coupon doesn’t deny use of the application (A:N); impact stays within the Juice Shop component (S:U).
- CWE-639
- OWASP A01:2021 - Broken Access Control