Cross-user basket access via /rest/basket/:id (IDOR)
retrieveBasket fetches a basket by request-supplied id without verifying the caller owns that basket, allowing any authenticated user to read another user's basket and its products.
In routes/basket.ts, the retrieveBasket handler attached to app.get('/rest/basket/:id', ...) (server.ts line ~594) does:
const id = req.params.id
const basket = await BasketModel.findOne({ where: { id }, include: [{ model: ProductModel, paranoid: false, as: 'Products' }] })
The lookup is keyed solely on the URL-supplied id. The session user is read via security.authenticatedUsers.from(req) but that user is only consumed inside the challengeUtils.solveIf(challenges.basketAccessChallenge, ...) callback — the application uses the mismatch (user?.bid != parseInt(id, 10)) to score the attack as a CTF challenge, not to deny the request. The basket (including products) is then returned to the caller via res.json(utils.queryResultToJson(basket)) with no ownership check.
Unlike the address routes, this endpoint is not wrapped in security.appendUserId(), and the BasketModel query is not scoped by the session user. The path is mounted with security.isAuthorized() only (server.ts line ~393), which checks that someone is logged in, not that they own the requested basket.
- Authenticate as user A and observe the basket id assigned (
/rest/user/whoamireturnsuser.bid). - Send
GET /rest/basket/<other_user_bid>with user A's JWT in the Authorization header. - Server returns the other user's basket including all product line items (and increments the
basketAccessChallengesolve, confirming the cross-user read).
Any authenticated customer can enumerate basket ids and read every other customer's basket contents (product selection, quantities, etc.). No additional privilege is needed beyond a valid login. This is a horizontal privilege escalation / IDOR.
The handler reads req.params.id and calls BasketModel.findOne({ where: { id } }) with no ownership filter, then returns the result via res.json. The session user from security.authenticatedUsers.from(req) is only referenced inside challengeUtils.solveIf(...) to detect/score the cross-user access (the user?.bid != parseInt(id, 10) comparison is purely the CTF trigger), and the route is mounted only behind security.isAuthorized() which checks authentication, not ownership. This is the canonical OWASP Juice Shop basketAccessChallenge IDOR, and the PoC (GET /rest/basket/<other_bid> with a valid JWT) works exactly as described.
The GET /rest/basket/:id endpoint is reachable over HTTP (AV:N) and only gated by security.isAuthorized() (mounted at server.ts ~line 393), which confirms a valid session but not basket ownership — so any logged-in customer can exploit it (PR:L, UI:N, AC:L). The handler uses BasketModel.findOne({ where: { id } }) keyed solely on the URL parameter, with the session user only consumed inside challengeUtils.solveIf(...) for scoring rather than for authorization, allowing an attacker to enumerate sequential basket ids and read every other user's basket contents (C:L — disclosure of basket/product data the attacker fully chooses, but limited in sensitivity to cart contents rather than credentials/PII). The handler only reads and returns data — it does not modify the basket and the query work is trivial, so I:N and A:N. Scope is unchanged as the impact stays within the Juice Shop application's own authority (S:U).
- CWE-639
- OWASP A01:2021 Broken Access Control