Checkout fetches basket by URL id with no ownership check (basket-access IDOR)
POST /rest/basket/:id/checkout fetches the basket using the path id without verifying the authenticated user owns it, letting any logged-in attacker check out another user's basket.
routes/order.ts:33-36:
return (req: Request, res: Response, next: NextFunction) => {
const id = req.params.id
BasketModel.findOne({ where: { id }, include: [{ model: ProductModel, paranoid: false, as: 'Products' }] })
.then(async (basket: BasketModel | null) => {
if (basket != null) {
id is the path parameter. The lookup is keyed only on the basket primary key — there is no UserId: req.body.UserId constraint in the where, and no follow-up if (basket.UserId !== req.body.UserId) return 403. The /rest/basket group applies security.isAuthorized() and security.appendUserId() (server.ts:350), so the request is authenticated and req.body.UserId is reliably the caller — but that value is never compared against basket.UserId.
The handler then mutates state on behalf of whichever basket was loaded: it generates an order PDF with the caller's email but containing the victim's basket items, decrements the victim's stock quantities (QuantityModel.update), and (since req.body.UserId is the caller after appendUserId) charges the attacker's wallet for the victim's items and credits bonus points. The basketAccessChallenge constant referenced in routes/basket.ts:21 confirms this is the known IDOR shape; the same flaw is present (and unmitigated) in the checkout route.
Safe forms: scope the query — BasketModel.findOne({ where: { id, UserId: req.body.UserId } }) — or check ownership after fetch and return 403 on mismatch.
- Authenticate as attacker
eve@example.comto obtain a JWT and learn her own basket id (say7). - Enumerate / guess another basket id
n(small integers; baskets are sequential). POST /rest/basket/<n>/checkoutwith the attacker's Authorization header and{ "orderDetails": { "paymentId": "wallet" } }.- The server fetches basket
n(the victim's), generates an order PDF for the victim's items, decrements stock, and processes payment — confirming the victim's basket contents and enabling order-on-behalf attacks.
Authenticated user can read another user's basket contents, trigger checkout against the victim's stock reservations, and produce order artifacts (PDF in ftp/) tied to the victim's basket. Combined with the in-app FTP browsing it leaks order details cross-tenant.
The placeOrder handler executes BasketModel.findOne({ where: { id }, ... }) using only req.params.id with no ownership constraint and no post-fetch check comparing basket.UserId to req.body.UserId (which appendUserId sets to the authenticated caller). The handler then mutates basket state, generates a PDF, decrements stock, and processes wallet payment, so any authenticated user can target another user's basket id. Scope explicitly says not to downgrade Juice Shop findings, and this matches the known basketAccessChallenge IDOR pattern.
The route is reachable over HTTP and the /rest/basket group applies security.isAuthorized()/security.appendUserId() (per the finding), so any authenticated user can call it (AV:N, PR:L, UI:N). BasketModel.findOne({ where: { id } }) at line 35 keys only on the path parameter — basket ids are small sequential integers, so enumeration is trivial (AC:L). Confidentiality is Low because the attacker learns another user's basket contents via the generated PDF/order record but not arbitrary system data; Integrity is Low because the handler mutates victim-owned state (clears their basket via BasketItemModel.destroy({ where: { BasketId: id } }), decrements QuantityModel stock for the victim's items, writes order artifacts to ftp/) though the attacker can't pick arbitrary values; Availability is Low since the victim's basket is destroyed and stock for those products is decremented, but the system itself stays up. Scope is Unchanged — impact stays within the Juice Shop app boundary.
- CWE-639
- OWASP A01:2021 Broken Access Control