Back to projects

Xsploit CTF

Full-stack CTF competition platform. Each challenge ships with its own in-browser sandboxed terminal, spawned on demand as an isolated Docker container and streamed over WebSocket to xterm.js. Supports teams or solo play, six challenge categories, leaderboards, and a staff admin panel.

Django Django Channels Daphne Next.js 16 TypeScript PostgreSQL Neon Docker xterm.js Render Vercel

The Idea

Xsploit is a full-stack Capture the Flag platform for hosting and managing cybersecurity competitions. Organizers create events and challenges, participants register individually or form teams, and the platform handles authentication, flag submission, scoring, and leaderboards end to end.

It is a personal project. Functional today, and built to be improved over time as I use it and add features.

Flagship Feature: In-Browser Sandboxed Terminals

Every challenge page includes an optional terminal panel that gives the competitor a real shell running inside a disposable, locked-down Linux container. No SSH keys, no VPN, no local Docker install. The user clicks "Boot the sandbox" and gets a working terminal in the browser within a couple of seconds.

Under the hood, the Django backend runs on Daphne (ASGI) with Django Channels for WebSocket support. When a user opens the terminal, the frontend establishes a WebSocket to the backend, an authentication middleware reads the access-token cookie, and a Channels consumer shells out to the Docker socket to spawn a fresh sandbox container. stdin and stdout are piped straight through the WebSocket to xterm.js in the browser.

  • Each sandbox runs from a dedicated image (xsploit-ctf-sandbox) with --network none, capped at 256 MB RAM, 0.5 CPU, and a PID limit.
  • Containers use --rm, so they are destroyed the moment the user disconnects.
  • An inactivity watchdog kills idle containers after 15 minutes.
  • The browser lazy-loads xterm.js and its fit addon at runtime, so the terminal only pays its JS cost on challenges that use it.

Platform Features

  • Competitions and events with per-event registration, either as a team or solo.
  • Challenges across six categories: web, crypto, pwn, rev, forensics, misc.
  • Flag submission with per-team or per-user solve attribution. If a user is on a team, the first member to solve a challenge counts for the whole team.
  • User and team leaderboards, optionally filtered to a specific competition.
  • Staff admin panel at /accounts/admin/ for managing competitions, challenges, and teams.
  • Three UI themes (default, dark, and an xsploit navy/purple theme) driven entirely by CSS custom properties.

Architecture

Monorepo with a Django REST + Channels backend and a Next.js 16 frontend. Docker Compose wires everything together for development; production is a split deployment across Vercel, Render, and Neon.

LayerTechnologyPurpose
FrontendNext.js 16 + TypeScriptApp Router pages, CSS Modules, edge auth guard in proxy.ts
Terminal UIxterm.jsLazy-loaded terminal emulator for challenge sandbox
Backend (HTTP)Django + DRFREST API for auth, competitions, challenges, teams, solves
Backend (WS)Django Channels + DaphneWebSocket consumers for the sandbox terminal
SandboxDockerBackend spawns per-session containers via the Docker socket
DatabasePostgreSQLHosted on Neon in production
Frontend hostVercelProduction deploy
Backend hostRenderProduction deploy, ASGI on Daphne

Security Design

  • Flags are SHA-256 hashed on save and never stored or returned as plaintext. The serializer marks the flag field write_only; the submission view hashes the incoming attempt before comparing.
  • Sandbox containers run with --network none, capped memory and CPU, a PID limit, and --rm, so a compromised sandbox cannot reach the internet, other containers, or persist data.
  • Auth cookies (access_token and refresh_token) are httpOnly. Route protection happens at the edge in proxy.ts rather than in React useEffect hooks, which avoids both flash-of-unauth content and redirect race conditions.
  • Sensitive endpoints (login, registration, flag submission, password reset) are rate-limited with django-ratelimit.
  • A shared Axios instance handles CSRF tokens automatically on unsafe methods and auto-refreshes access tokens on 401, queueing concurrent requests during refresh. Raw fetch is never used.
  • All database access goes through the Django ORM, so user-supplied strings (team codes, flag attempts, usernames) are never interpolated into raw SQL.

Key Decisions

  • Django Channels over REST polling. The sandbox terminal needs a persistent bidirectional stream, and Channels lets the same Django app own both HTTP and WebSocket concerns without a second service.
  • Daphne instead of Gunicorn. Gunicorn does not handle WebSockets, and running two servers side by side adds deployment complexity I did not want.
  • Next.js proxy.ts (Next 16) instead of middleware.ts. middleware.ts is deprecated in Next 16; keeping both alive at once caused infinite-loading bugs during the upgrade.
  • Cookie-based auth instead of bearer tokens in headers. httpOnly cookies protect tokens from JavaScript, and the WebSocket hand-off reads the cookie from the connection scope directly.
  • Dev uses Docker Compose; prod uses Vercel + Render + Neon. The dev stack keeps the Docker-socket sandbox functionality intact locally, while the split prod deployment gives each tier the host that fits it best.

Status

Currently deployed to Vercel (frontend), Render (backend on Daphne), and Neon (Postgres). The repository is private for now. The platform is functional end to end, with ongoing work focused on polish, additional challenge tooling, and eventually running a real competition on it.