Skip to content

Releases: gl0bal01/ctfd-warboard

v0.4.0

23 Jun 07:03

Choose a tag to compare

Security

  • Dependency advisories — override transitive undici to ^6.27.0
    (GHSA-vxpw-j846-p89q) and ws to ^8.21.0 (GHSA-96hv-2xvq-fx4p), both high
    severity, reached via discord.js. bun audit --audit-level=high is clean.
  • No raw error text to usersformatInteractionError no longer echoes
    arbitrary error messages to Discord replies (could leak filesystem paths,
    remote URLs, or git command lines); full errors stay in the logs.

Added

  • Typed CTFd errorsCtfdAuthError / CtfdApiError in core, thrown by the
    CTFd client and matched via instanceof (with cause-chain unwrapping)
    instead of brittle message-string sniffing.
  • Shared Discord reconcilerensureChallengeThreads,
    textChannelsUnderCategory, and fetchThreadNames consolidate the
    category-channel + per-challenge thread logic used by /ctf start,
    /ctf refresh, and the poller. Dedup includes archived threads.
  • RepoManager.countChallenges() — parallel repo walk counting
    .challenge-id markers; /ctf status now reports CTFd / Git / Discord counts.
  • Operator toolingscripts/install-vps.sh hardened systemd installer,
    make verify-live smoke-test checklist, and grouped make help.

Changed

  • Bot requires SSH remotesvalidateGitRemoteUrl({allowHttps}); the bot
    rejects HTTPS git remotes (it runs non-interactively and can't supply
    credentials). Behavior change for bot users on HTTPS remotes. The CLI
    still allows HTTPS for interactive use.
  • /ctf status responsiveness — defers the reply, runs per-CTF cache load,
    repo walk, and Discord fetch concurrently, and counts threads via a live API
    fetch (restart-safe; no longer reports 0 from a cold gateway cache).
  • ensureChallengeThreads contract — requires a defined categoryId; the
    poller guards an unset category so it can no longer create orphan channels at
    the guild root.

Fixed

  • Duplicate threads after auto-archive — the dedup set now includes archived
    threads, so a thread that auto-archived (7-day default) is not recreated on the
    next refresh/poll.
  • Partial-init visibility/ctf start surfaces a shortfall warning when
    some challenge threads fail to create, instead of reporting success.

Removed

  • docs/migration.md (pre-v0.3.0 upgrade path) and docs/release.md
    (internal release checklist).

Fixed (anti-slop cleanup pass)

  • Slug-collision guard correctness.challenge-id now written with O_EXCL;
    EACCES/EIO/EISDIR on read no longer silently bypass the collision check.
  • Lock acquire error classification — non-EEXIST errors (EACCES, ENOSPC,
    EROFS, EISDIR) surface immediately instead of busy-looping until timeout.
  • Bot commit-push lock cascadecommitAndPush now acquires + releases
    the FileLock per closure; chained pushes no longer hit a 5s lock-acquire
    timeout when commits land within the debounce window.
  • Poller resilience — reschedule moved into finally so a synchronous
    throw cannot kill polling; each thread-create is wrapped per-iteration so one
    bad challenge no longer poisons forward progress.
  • Poller TOCTOUmeta is plumbed into createThreadsForNewChallenges
    instead of refetched via getActiveCTF (which would throw if the CTF was
    archived between the loop start and the call).
  • ctf-start ordering and cleanup — guild check runs before lock.acquire;
    Discord channel creation block is wrapped so a mid-loop failure deletes the
    partial category instead of leaving orphans.
  • ctf-start download error visibility — download phase failure now appears
    in the user-facing reply instead of being silently logged.
  • CTFd auth-redirect detectionisAuthRedirect parses the location as a
    URL and checks pathname/searchParams.has("next") instead of brittle
    substring matches (?nextpage= no longer false-positives; CTFd installs at
    non-root paths like /ctf/login are correctly detected).
  • DNS-rebinding mitigation TTLvalidatedOrigins cache is bounded to 60s
    so a one-time public-IP lookup cannot defeat the SSRF guard forever.
  • CLI password prompt SIGINT — Ctrl-C / Ctrl-D in the raw-mode prompt now
    restores terminal state instead of leaking raw mode.
  • CLI writeup editor exit — non-zero editor exit (e.g. :cq) logs a warning
    instead of producing an unhandled promise rejection.
  • CLI refresh auth-error hint — adds a session-expiry hint when the error
    looks auth-related.
  • Bot active-CTF resolutiongetActiveCTF no longer silently falls back
    to active[0]. Behavior change: /ctf-refresh and /ctf-archive
    invoked outside a challenge thread now require currentCTF to be set (via
    /ctf-use or per-thread context); previously they would pick the first
    active CTF found. Intended as a safety improvement — prevents writes to a
    random CTF when the caller's context fails to resolve.

Fixed (follow-up cleanup)

  • /claim race-safety — team-mapping and claim writes for the same CTF
    now run under a per-CTF FileLock. Concurrent /claim invocations no longer
    drop the loser's team mapping or interleave their writes.
  • /ctf start ordering — commit happens first, then registerCTF, then
    push. A push failure no longer leaves a remote scaffold without a local
    record; the user can recover with a manual git push.
  • Auth-resolver input narrowedresolveAuthAndClient now accepts a
    structural AuthInput ({url, name?, token, sessionCookie, username})
    instead of a full CTFMeta. init and ctf start no longer fabricate
    placeholder repo: "" / repoPath: "" / status: "active" fields.
  • Shared download-summary formatterformatDownloadSummary() in core
    produces the headline/lines/overflow shape consumed by both CLI init and
    bot /ctf start.
  • Shared Discord rate-limit constantDISCORD_THREAD_CREATE_DELAY_MS
    exported from bot/index.ts and used by both poller and ctf-start.

Added

  • Persistent active CTF pointer for CLIctfd use <name> now writes
    currentCTF in global config and command resolution honors it.
  • CTF Discord routing metadata — CTF meta now stores Discord guild/category
    identifiers so poller and thread commands can target the correct CTF context.
  • Download summary plumbingdownloadChallengeFilesForCTF() now returns
    downloaded/skipped counts with skip reasons; callers surface/report skips.
  • Challenge slug-collision guardRepoManager now writes and validates
    per-directory .challenge-id markers to prevent silent overwrite when
    different challenges map to the same slug path.

Changed

  • Poll scheduling model — bot poller now runs serialized recursive ticks
    (no overlapping setInterval async executions).
  • CLI and bot repo path consistency — CLI init/join now use
    repoPathFor(ctfName) under data root, matching bot behavior.
  • Service install semanticsmake service-install no longer errors on an
    existing /etc/ctfd-warboard/bot.env; deployment docs updated accordingly.
  • Challenge markdown file links — generated links now strip query/hash
    components (e.g. token query parameters) before writing to git.

Fixed

  • Critical lock raceFileLock.acquire() now uses atomic wx lockfile
    creation, preventing concurrent contenders from both acquiring a fresh lock.
  • CLI credential durability — init persists the actual username for
    password-based auth; refresh now uses shared auth resolver and persists
    refreshed session cookies.
  • CLI writeup editor flowctfd writeup now waits for editor process exit.
  • Bot /solve CTF mismatch — solve now resolves CTF from thread context and
    requires the challenge to exist in the active CTF cache before submission.
  • Cross-process git safety for bot writes — bot commit/push path now acquires
    the same file lock family used by CLI operations.
  • /ctf start race window — per-CTF lock is acquired before Discord/channel
    creation and initialization workflow.
  • Poller channel misrouting — new-thread creation is scoped to stored
    guild/category instead of matching category names across all guilds.
  • Cache diff blind spot — challenge file list changes are now detected as
    updates.
  • Validation side effect — removed console.warn from URL validation.

v0.3.0 — community readiness: operator surface, data-root, parity, fixes

15 May 10:36

Choose a tag to compare

Full community-readiness release. All Phases 2–6 of the readiness plan land together per the v0.3.0 ADR (atomic Phase 2 + 2.5 constraint; Phases 3–6 within the same release).

Upgrading from v0.2.x? See docs/migration.md. Existing operators on systemd must mv repos to /var/lib/ctfd-warboard/repos/ (or accept the auto-WARN with mv instructions) and re-run make service-install.

Operator surface (Phase 2)

  • Makefile at repo root with 17 targets — single entrypoint for install / check / bot / service-* / migrate-data-root.
  • .env.example + scripts/ctfd-bot.env.example documenting all bot env vars (no real secrets).
  • docs/deployment.md — full VPS setup: dedicated user, FHS layout, env file, systemd install, journald, SSH key, backups, log rotation, secret rotation, mixed bot+CLI access.

Data root + hardened systemd (Phase 2.5, atomic)

  • packages/core/src/paths.ts — shared resolver: dataRoot(), repoPathFor(), lockPathFor(), assertDataRootWritable(). Honors CTFD_WARBOARD_HOME; absolute-path validation; mkdir -p mode 0700; path-traversal rejection via allowlist regex; boot-time writability check (FATAL + exit 1 before Discord connect).
  • Bot + CLI share data root via CTFD_WARBOARD_HOME. Bot CONFIG_DIR derived from dataRoot(). Bot ctf-start.ts and ctf-join.ts use repoPathFor(ctfName) instead of cwd-based resolve(ctfName).
  • ConfigManager lazy-once meta.repoPath migration — rewrites persisted CTFMeta entries whose repoPath falls outside dataRoot(); WARN log with old→new + mv instruction (emitted only after rename succeeds); idempotent.
  • make migrate-data-root — triggers migration without starting the bot.
  • Hardened systemd unit (scripts/ctfd-bot.service) — User=ctfd-warboard, EnvironmentFile=, Environment=CTFD_WARBOARD_HOME=/var/lib/ctfd-warboard, ProtectSystem=strict, ReadWritePaths=/var/lib/ctfd-warboard, NoNewPrivileges=true, PrivateTmp=true, ProtectHome=true. No inline secrets.
  • make service-install code-level gate — refuses to install the strict unit if packages/core/src/paths.ts is absent (filesystem stat).
  • Cross-process lock unification — bot and CLI both resolve lock paths through lockPathFor(); concurrent ops on shared CTFD_WARBOARD_HOME serialize correctly.

Bot ↔ CLI parity (Phase 3)

  • downloadChallengeFilesForCTF in @ctfd-warboard/core — shared download orchestration helper.
  • Bot /ctf start now downloads CTFd challenge files (identical layout to ctfd init), with Discord interaction editReply progress edits every 10 challenges. Downloads run inside the per-ctfName FileLock, before the git transaction.
  • CLI init.ts now also uses the shared helper — single source of truth.

Functional fixes (Phase 4)

  • CLI ctfd claim uses resolveChallenge for category/challenge resolution (web/sql-injection now resolves correctly to category web).
  • CLI --version reads dynamically from packages/cli/package.json.
  • resolveAuthAndClient(meta, opts?) in @ctfd-warboard/core — shared auth helper. Priority: meta.tokenmeta.sessionCookie (verified; 401 falls through) → username + password. Bot /ctf start, bot helpers.ts, CLI solve, CLI init all migrated. loginWithCredentials direct callsites in packages/bot and packages/cli removed.
  • CLI ctfd solve now supports all auth types (was token-only).
  • helpers.ts error rewrap narrowed — only emits "No credentials found" when no auth method exists; otherwise re-throws original error with cause (DNS/TLS/SSRF surface correctly).

CI + release docs (Phase 5)

  • CI Bun version pinned to 1.3.13 in .github/workflows/ci.yml; .tool-versions added for local reproducibility.
  • CI runs make check (single entrypoint matches local).
  • docs/release.md — release checklist with concrete shell commands.

README (Phase 6)

  • New sections: "5-minute local bot smoke test", "Production install on Ubuntu with systemd", "How secrets are stored", "Known limitations", "Troubleshooting". Cross-links to docs/deployment.md, docs/migration.md, docs/release.md.

Verification

  • 239 tests pass / 0 fail across 28 files.
  • bun run lint, bun run typecheck, bun audit --audit-level=high → exit 0.
  • systemd-analyze verify scripts/ctfd-bot.service → exit 0.
  • Acceptance greps: resolve(ctfName)\|resolve(repoName) in packages/bot/src → empty; loginWithCredentials in packages/bot packages/cli → empty.

Known limitations

  • VM smoke test under the strict systemd unit is deferred — operators are encouraged to exercise the hardened unit before mass adoption and append any required additional ReadWritePaths= entries.
  • Several NIT/MEDIUM follow-ups from the v0.3.0 code review (memoize mkdirSync, narrow auth-resolver re-login to also cover 403, ctf-start scaffold-before-lock, etc.) are filed as future work.

Full Changelog: v0.2.1...v0.3.0

v0.2.1 — Phase 1 community readiness blockers

15 May 10:36

Choose a tag to compare

Security and reliability patch. Adopt this release before v0.3.0 — it ships the CVE fix and git-sync correctness fixes independently of the larger v0.3.0 runtime rewrite.

Security

  • Pinned simple-git to exact 3.36.0 across all three workspaces (core, cli, bot), resolving GHSA-hffm-xvc3-vprc (ReDoS in <3.36.0).

Fixed

  • Bot git-sync push-before-commit ordering corrected across all 8 commitAndPush callsites in packages/bot. Push is now chained to the latest debounced commit via enqueuePushAfter, so a push can never race ahead of its commit.
  • Push silently dropped on back-to-back commitAndPush calls (HIGH): removed pushPending guard from enqueuePush; the FIFO queue serialises pushes and enqueuePushAfter's commit-level debounce handles coalescing.
  • drain() skipping chained push on SIGTERM (MEDIUM): drain() now flushes pendingPushFn alongside pendingCommitFn.
  • /ctf start initial repo flow is now an atomic locked transaction: a per-ctfName FileLock is acquired before init, ensuring concurrent invocations for the same CTF cannot double-initialise the repository.

Internal

  • enqueueTransaction(fns: AsyncFn[]) added to GitSyncQueue in @ctfd-warboard/core. Executes an ordered sequence of async operations inside the queue, propagating the first error and aborting on throw.

Verification

  • 209 tests pass / 0 fail.
  • bun audit --audit-level=high → 0 vulnerabilities.

Full Changelog: v0.2.0...v0.2.1

v0.1.0

22 Mar 13:59

Choose a tag to compare

First release

Rate-limited CTFd parser with CLI + Discord bot for small-team CTF collaboration. Every action auto-syncs to a shared private git repo.

Features

  • Rate-limited API client — 1 req/sec with exponential backoff on 429s
  • Cache-first — fetches once, polls for changes every 10 minutes
  • Full challenge details — descriptions, files, hints fetched and stored
  • File downloads — challenge attachments downloaded during init
  • Git-native collaboration — claims, solves, notes auto-commit to shared repo
  • Discord bot — slash commands for team coordination (/ctf start, /claim, /solve, /note, /writeup)
  • CLI — terminal workflow with ctfd init, ctfd claim, ctfd solve, etc.
  • Multi-CTF — manage multiple competitions simultaneously
  • Private channels — Discord CTF channels are private by default
  • Direct CTFd links — challenge threads and markdown include links to the live challenge

Authentication

  • API token auth (--token or CTFD_TOKEN)
  • Username/password auth (--username/--password or CTFD_USERNAME/CTFD_PASSWORD)
  • CSRF nonce handling for CTFd instances requiring session auth
  • Lazy CSRF token fetch for flag submission

Security

  • Path traversal protection
  • SSRF blocking for private IPs
  • Credential isolation from git repos
  • bot.json with 0600 permissions