Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

108 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Web eXploitation Laboratory (WXL)

A fully browser-based, WebAssembly-powered web exploitation training platform — no backend server required.

Quality Gates License: ECL-2.0 VitePress pnpm

Overview

Web eXploitation Laboratory (WXL) is a CTF-style web exploitation training platform. Every challenge runs entirely in the browser: WebAssembly simulates a realistic backend environment so the platform can be deployed and used without any server infrastructure.

Core features

  • Pure-frontend execution: a Service Worker intercepts HTTP requests and emulates backend behavior in the browser.
  • Multiple backend runtimes: Python Flask / FastAPI (via Pyodide) and PHP (via php-wasm) are all supported.
  • Encrypted virtual filesystem: flags and application assets are stored under AES-GCM-256 encryption, preventing direct reads.
  • Static deployment: build output is plain static files and can be hosted on any static service (GitHub Pages, Cloudflare Pages, etc.).

Tech stack

Layer Technology
Documentation framework VitePress 2.0.0-alpha.16
UI framework Vue 3 3.5 + UnoCSS
State management Pinia 3
Python runtime Pyodide 0.29
PHP runtime php-wasm
WASM modules Rust 2021 + wasm-pack
Attack session tracking IndexedDB (idb package) attack-session persistence
Package manager pnpm 10.28

Prerequisites

  • Node.js >= 22.6 — challenge:keygen, create:challenge, challenge:validate, and challenge:analyze run through node --experimental-strip-types, which is unavailable on earlier releases. The remaining TypeScript scripts go through the bundled tsx and have no such floor. CI builds on Node 24.

  • pnpm >= 10 (npm install -g pnpm)

  • Rust toolchain (install via rustup)

  • wasm-pack (cargo install wasm-pack) — not a package dependency; install it into your Rust toolchain

  • wasm-tools (cargo install wasm-tools --version 1.249.0 --locked, or pnpm wasm:tools) — pinned to the same version CI installs; required by the L2 stage of pnpm challenge:verify, which runs wasm-tools validate on the generated payload. pnpm challenge:keygen also uses it for its strip and mutate passes, but degrades to a warning for each when it is absent.

  • Chromium for Playwright — required before the first pnpm challenge:verify or pnpm test:smoke run. After pnpm install, install the browser binary once with:

    pnpm exec playwright install chromium

Quick start

# 1. Clone the project
git clone https://github.com/BrowserLaboratory/wxl-template.git
cd wxl-template

# 2. Install Node.js dependencies
pnpm install

# 3. Build the WASM modules and start the dev server
pnpm dev

The dev server starts at http://localhost:5173 by default.

Available scripts

Command Description
pnpm dev Build the WASM modules and start the dev server
pnpm build Build the WASM modules and emit the static site
pnpm docs:dev Start the VitePress dev server only (skips the WASM build)
pnpm docs:build Build the VitePress static site only
pnpm docs:preview Preview the built static site
pnpm test Run the TypeScript / JavaScript unit tests (Vitest). Bare, it stays in watch mode — use pnpm test --run for a single pass
pnpm test:smoke Run the Playwright smoke tests against the built site
pnpm wasm:build Build every Rust WASM module
pnpm wasm:test Run the Rust unit tests (cargo test)
pnpm wasm:tools Attempt to install wasm-tools into the Rust toolchain; it silences failures and always exits 0, so confirm with wasm-tools --version
pnpm fork:init Rewrite the project identity after forking (author, GitHub URLs, and optionally the VitePress base). Requires --author and --repo; the package name changes only with --name or --rebrand, and SITE_BASE in deploy.yml is never touched
pnpm challenge:keygen Generate the encrypted WASM module for every challenge
pnpm create:challenge Scaffold a new challenge from flags; --name <slug> is required and a bare run exits 1 with its usage line
pnpm challenge:validate Validate every challenge's frontmatter and file layout
pnpm challenge:analyze Report the content and configuration of a challenge
pnpm challenge:retype Mutate an existing challenge's backend / difficulty / tags / category
pnpm challenge:verify Run the layered verify gate (L1 lint, L2 build, L3 Playwright e2e) on a challenge
pnpm challenge:verify:blind Run the L4 blind-solve sub-routine standalone (also reached via pnpm challenge:verify <slug> --blind)
pnpm challenge:verify:cross Maintainer-only L4 multi-agent cross-check — runs the blind gate against claude,codex,gemini and aggregates verdicts
pnpm prepare Install the git hooks (simple-git-hooks); runs automatically after pnpm install

Architecture

Browser
├── VitePress site (Vue 3 + UnoCSS)
│   ├── Challenge pages (Markdown + YAML frontmatter)
│   └── IndexedDB (attack-session persistence + tool state)
├── Service Worker (docs/public/challenge-sw.js)
│   └── Intercepts HTTP requests and routes them to the matching WASM runtime
└── WASM runtimes
    ├── virtual-fs      Encrypted virtual filesystem (Rust)
    ├── asgi-bridge     Python ASGI/WSGI bridge layer (Rust)
    ├── wxlsh-parser    wxlsh terminal command parser and native commands (Rust)
    ├── python-bridge   Pyodide integration (TypeScript)
    └── php-bridge      php-wasm integration (TypeScript)

The three Rust crates live under chall-wasm/ and are built by pnpm wasm:build.

Request flow

  1. The user interacts with a challenge page, triggering an HTTP request to the "backend".
  2. The Service Worker intercepts the request and routes it to the right runtime per the challenge configuration.
  3. The Python runtime or the PHP runtime handles the request and returns an HTTP response.
  4. The challenge page renders the result.

Challenge configuration format

Every challenge is a Markdown file with a YAML frontmatter block declaring its configuration:

---
title: Door Is Open      # required
layout: challenge        # selects the challenge UI; without it the page renders as a plain docs page
backend: fastapi         # required — flask | fastapi | php
app: app.py              # required — path relative to the challenge's src/ root
difficulty: easy
category: web
packages: []             # extra Python packages to install via micropip
tools: [ browser, network, repeater, code ]   # omit the field to get exactly these four;
                                              # Terminal appears only when you list it, and
                                              # browser is added back if you leave it out
source_visible: false    # true = white-box, false = black-box (default)
wasmModule: /challenge/door-is-open/runtime.wasm  # produced by keygen
---

Challenge source files are picked up by scanning the challenge's src/ directory. The legacy fs: mapping is still accepted for compatibility, but the validator emits a deprecation warning — new challenges SHALL rely on the src/ scan instead.

Contributing

See CONTRIBUTE.md for the branching strategy, PR workflow, and commit conventions.

Deployment

Build output lives in .vitepress/dist/ as plain static files and can be deployed to any static hosting service.

Build steps

# 1. Install dependencies
pnpm install

# 2. Full build (WASM + keygen + VitePress)
pnpm build

Deploying to GitHub Pages

This repository ships a working deployment workflow at .github/workflows/deploy.yml — use it as-is rather than writing your own. Its shape:

  • Trigger: a v* release tag push, plus manual workflow_dispatch. Pushing to main does not deploy.
  • Pages source: the GitHub Actions deployment method (actions/upload-pages-artifact + actions/deploy-pages). There is no gh-pages branch.
  • Toolchain: Node 24 and a SHA-pinned wasm-pack 0.14.0, matching release.yml.
  • Base path: the build step sets SITE_BASE: /wxl-template/.

SITE_BASE is what makes a project site work. VitePress bakes it into every asset URL, so a site served from https://<user>.github.io/<repo>/ needs SITE_BASE: /<repo>/ — without it, the deployed page requests its assets from the domain root and every one of them 404s. Set it only in the deploy workflow; leaving it unset keeps local and CI builds rooted at /.

Two settings live in the GitHub UI rather than in this repository:

  1. Settings → Pages → Source must be set to GitHub Actions.
  2. The github-pages environment must allow deployments from v* tags (in addition to the main branch, which authorises workflow_dispatch runs). Without that rule, the tag-triggered deploy job is rejected.

After forking, run fork:init with its required flags to rewrite the project identity:

pnpm fork:init --author "<Your Name>" --repo <owner>/<repo>

It rewrites package.json and the GitHub URLs. It does not touch SITE_BASE. The script refuses to overwrite an existing .github/workflows/deploy.yml — and a fork always has one — so it prints left untouched and moves on, leaving SITE_BASE: /wxl-template/ in place. Edit that value to /<repo>/ yourself, or the deployment 404s exactly as described above.

The example omits --base deliberately: with the flag absent the script leaves .vitepress/config.mts alone, so the env-conditional base: process.env.SITE_BASE ?? '/' survives and SITE_BASE stays in control. Passing --base /<repo>/ instead replaces that line with a hard-coded literal, after which SITE_BASE no longer influences the build. Do not reach for --base none expecting the env-conditional form — none deletes the base declaration outright, so VitePress falls back to / and SITE_BASE becomes a no-op, which is the 404 case described above.

Deploying to Cloudflare Pages

  1. Create a new project on Cloudflare Pages and link the GitHub repository.

  2. Set the build command:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal \
      && . "$HOME/.cargo/env" \
      && cargo install wasm-pack \
      && pnpm install \
      && pnpm build
  3. Set the output directory to .vitepress/dist.

  4. Add NODE_VERSION=24 to the environment variables.

  5. If the site is served from a sub-path, add SITE_BASE with that path (leave it unset for a root-domain deployment).

Note: Cloudflare Pages does not ship a Rust toolchain by default. wasm-pack is a Rust binary, not a package dependency — pnpm install will not provide it — so the build command above installs the minimal Rust toolchain along with wasm-pack before building. wasm-tools is not installed here. pnpm build does run pnpm challenge:keygen, which uses wasm-tools for its strip and mutate passes, but keygen degrades to a warning for each when the tool is absent — so the build still succeeds, just without those passes. Add wasm-tools to the install line (cargo install wasm-tools --version 1.249.0 --locked, the same version CI pins) if you want stripped and mutated payloads in production builds.

License

This project is licensed under the Educational Community License, Version 2.0 (ECL-2.0).

About

A serverless, browser-only template for building WebAssembly-powered web-exploitation CTF challenges — VitePress + Vue 3 + Pyodide + php-wasm.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages