BugForge — 2026.03.31

Tanuki: IDOR on User Statistics Endpoint

BugForge Broken Access Control easy

Overview

  • Platform: BugForge
  • Vulnerability: Insecure Direct Object Reference (IDOR) on User Statistics Endpoint
  • Key Technique: Path parameter manipulation on /api/stats/:userId to access other users’ data without ownership validation
  • Result: Enumerated all seed user stats and captured hidden achievement_flag field from user IDs 1-3

Objective

Find and exploit vulnerabilities in a flashcard study application to capture the flag.

Initial Access

# Target Application
URL: https://lab-1774991334271-678obv.labs-app.bugforge.io

# Auth details
Registered user: haxor (ID 4, role: user)
Auth mechanism: JWT HS256 (payload: {id, username, iat})

Key Findings

  1. Insecure Direct Object Reference on Statistics Endpoint (CWE-639: Authorization Bypass Through User-Controlled Key) — The GET /api/stats/:userId endpoint accepts an arbitrary user ID as a path parameter and returns that user’s statistics without verifying the authenticated user owns the requested resource. Any authenticated user can read any user’s stats by iterating IDs, including a hidden achievement_flag field only present on seed accounts.

Attack Chain Visualization

┌──────────────────┐     ┌─────────────────────┐     ┌──────────────────────┐
│  Register user   │     │ Map API surface     │     │  Test admin          │
│  (haxor, ID 4)   │────>│ via traffic capture │────>│  endpoints → 403     │
└──────────────────┘     └─────────────────────┘     └──────────┬───────────┘
                                                                │
                                                                v
                                                    ┌───────────────────────┐
                                                    │  Identify IDOR on     │
                                                    │  GET /api/stats/:id   │
                                                    │  (userId in path)     │
                                                    └───────────┬───────────┘
                                                                │
                                                                v
                                                    ┌───────────────────────┐
                                                    │  Request /api/stats/1 │
                                                    │  /api/stats/2         │
                                                    │  /api/stats/3         │
                                                    │  as user ID 4         │
                                                    └───────────┬───────────┘
                                                                │
                                                                v
                                                    ┌───────────────────────┐
                                                    │  200 OK — full stats  │
                                                    │  + achievement_flag   │
                                                    │  on seed users only   │
                                                    └───────────────────────┘

Application Architecture

Component Path Description
Frontend React SPA (MUI components) Flashcard study interface with deck management
Backend Express/Node.js REST API with JWT authentication
Database SQLite (assumed, sequential IDs) Users, decks, cards, study progress
Auth JWT HS256 Token contains {id, username, iat} — no role in claims, role stored DB-side

Exploitation Path

Step 1: Reconnaissance — Map the API Surface

Registered an account (haxor, assigned ID 4) and captured traffic to enumerate all API endpoints. The application is a spaced-repetition flashcard study tool with deck management, study sessions, and progress tracking.

Key endpoints discovered:

  • POST /api/register — User registration (returns JWT, no role in response)
  • POST /api/login — Authentication (returns JWT + user object with role field)
  • GET /api/verify-token — Token validation
  • GET /api/decks / GET /api/decks/available — Deck listing (3 available: Planets & Moons, Linux Trivia, Cheese Origins)
  • POST /api/decks/:id/add / DELETE /api/decks/:id/remove — Deck management
  • GET /api/study/:deckId/cards — Flashcards for study
  • POST /api/study/progress — Save card progress (spaced repetition: card_id, quality)
  • POST /api/study/session — Save study session (deck_id, cards_studied, correct_count)
  • GET /api/stats/:userIdUser statistics (userId in path parameter)
  • GET/POST/PUT/DELETE /api/admin/* — Admin CRUD for users, decks, cards

Key observations:

  • JWT contains {id, username, iat} with no role — role stored server-side only
  • Login response includes role: "user" — role exists in the user model
  • Sequential user IDs — seed users at IDs 1-3, our registration got ID 4
  • Stats endpoint takes userId as a path parameter rather than deriving it from the JWT
  • CORS wide open: Access-Control-Allow-Origin: *

Step 2: Test Admin Access (Dead End)

Attempted to access admin endpoints with our regular user JWT:

GET /api/admin/users HTTP/1.1
Host: lab-1774991334271-678obv.labs-app.bugforge.io
Authorization: Bearer <jwt>

Response: 403 Forbidden{"error":"Admin access required"}

Admin endpoints have proper server-side role enforcement. This ruled out simple broken access control on the admin panel.

Step 3: IDOR on Statistics Endpoint

The /api/stats/:userId endpoint stood out — it takes the user ID as a path parameter rather than deriving it from the authenticated JWT. This is a classic IDOR pattern.

Tested by requesting stats for user ID 1 while authenticated as user ID 4:

GET /api/stats/1 HTTP/1.1
Host: lab-1774991334271-678obv.labs-app.bugforge.io
Authorization: Bearer <jwt-for-user-4>

Response: 200 OK — returned full statistics for user ID 1, including an achievement_flag field not present on our own account.

Confirmed the same for IDs 2 and 3:

GET /api/stats/2 HTTP/1.1
Authorization: Bearer <jwt-for-user-4>
# 200 OK — stats + achievement_flag

GET /api/stats/3 HTTP/1.1
Authorization: Bearer <jwt-for-user-4>
# 200 OK — stats + achievement_flag

Our own stats (GET /api/stats/4) did not contain the achievement_flag field — it’s only present on seed accounts.

Step 4: Flag Captured

The achievement_flag field across all three seed users contained the same flag:

bug{Amp1w10WfTw2e4twMHj9JqDIIahssqxl}

Flag / Objective Achieved

bug{Amp1w10WfTw2e4twMHj9JqDIIahssqxl}

Obtained from the achievement_flag field in seed user statistics, accessed via IDOR on GET /api/stats/:userId.


Key Learnings

  • Path parameters holding user IDs are high-value IDOR targets — When an endpoint accepts a resource identifier in the URL path rather than deriving it from the authenticated session/token, always test whether it enforces ownership. The JWT had the user ID right there, but the endpoint ignored it.
  • Sequential IDs make enumeration trivial — With IDs 1-3 as seed users and our registration at ID 4, the entire user space was trivially enumerable. UUIDs wouldn’t fix the authorization bug, but they’d raise the bar for blind enumeration.
  • Hidden fields in API responses are not access control — The achievement_flag field was absent from the frontend UI but fully present in the API response. Security through omission from the UI is not security at all.

Failed Approaches

Approach Result Why It Failed
Broken access control on /api/admin/* 403 “Admin access required” Server enforces role check on admin endpoints

Tools Used

Tool Purpose
Caido HTTP interception, request replay, and endpoint enumeration
Browser DevTools Traffic capture and response inspection

Remediation

1. Insecure Direct Object Reference on Statistics (CVSS: 7.5 - High)

Issue: The /api/stats/:userId endpoint uses the path parameter directly for the database lookup without comparing it to the authenticated user’s ID from the JWT. Any authenticated user can access any other user’s statistics. CWE Reference: CWE-639 - Authorization Bypass Through User-Controlled Key

// BEFORE (Vulnerable)
app.get('/api/stats/:userId', authenticate, async (req, res) => {
  const stats = await db.get(
    'SELECT * FROM user_stats WHERE user_id = ?',
    [req.params.userId]
  );
  res.json(stats);
});

// AFTER (Secure) — Option A: Enforce ownership
app.get('/api/stats/:userId', authenticate, async (req, res) => {
  if (parseInt(req.params.userId) !== req.user.id) {
    return res.status(403).json({ error: 'Access denied' });
  }
  const stats = await db.get(
    'SELECT * FROM user_stats WHERE user_id = ?',
    [req.params.userId]
  );
  res.json(stats);
});

// AFTER (Secure) — Option B: Remove path param entirely
app.get('/api/stats', authenticate, async (req, res) => {
  const stats = await db.get(
    'SELECT * FROM user_stats WHERE user_id = ?',
    [req.user.id]  // Always use the authenticated user's ID
  );
  res.json(stats);
});

Option B is preferred — removing the path parameter eliminates the attack surface entirely. There’s no reason for a user to request stats for anyone other than themselves.


OWASP Top 10 Coverage

  • A01:2021 - Broken Access Control — IDOR on the statistics endpoint allows horizontal privilege escalation, accessing other users’ data without authorization checks

References


Tags: #idor #broken-access-control #api-security #information-disclosure #bugforge Document Version: 1.0 Last Updated: 2026-03-31

#IDOR #API #user-enumeration