-
v3.3.0
StableAll checks were successfulCI / Frontend tests, audit, and build (push) Successful in 1m18sCI / Bot tests and audit (push) Successful in 2m2sRelease / Create Forgejo release (push) Successful in 1m55sCI / Backend lint (ruff) (push) Successful in 2m17sCI / Docker image build (push) Successful in 2m48sCI / Backend migration, tests, and audit (push) Successful in 3m31sreleased this
2026-07-15 04:38:26 +00:00 | 776 commits to main since this releaseHardening: bugs & security.
Correctness and security fixes from the July 2026 full-project review. All 13
milestone issues; backend and bot test suites green.Security
- [webapp] Discord webhook SSRF allowlist —
discord_webhook_url(campaign
and admin-fallback) is validated with real URL parsing against a Discord host
allowlist (discord.com,discordapp.com,ptb.discord.com,
canary.discord.com; schemehttps; path/api/webhooks/) at write time,
and re-checked immediately before every server-side POST so legacy rows are
neutralised — a non-Discord URL is logged and skipped, never fetched. Closes a
server-side request forgery vector against the deployment's internal network. - [webapp+bot] Recording authorization —
/record start|stopnow require the
invoker to hold Discord Manage Server or be the campaign's verified-linked GM
(fails closed on lookup error); the audio-upload endpoint verifies the
session's campaign belongs to the suppliedguild_id(404 on mismatch, no
task queued), preventing cross-guild audio attachment. New
GET /api/bot/guilds/{guild_id}/gmssupports the bot-side check. - [webapp] Reject placeholder/weak secrets at startup —
SECRET_KEY(rejects
the.env.exampleplaceholder, requires ≥ 32 chars),DATABASE_URL/
DATABASE_MIGRATE_URL(rejectchangeme/changeme_migratepasswords, parsed
not substring-matched), andbot_api_key(rejects placeholders / < 16 chars
when set) fail fast with an actionable message. - [webapp] DML-only app DB role — the app now connects as a non-superuser
questboardrole (DML only);POSTGRES_USERbecomes a dedicated superuser
questboard_adminused only for break-glass;pg_dumpbackups run as the
schema-owningquestboard_migraterole. Breaking for existing installs —
see the "Existing Installs" runbook indocs/OPERATIONS.md. Requires
POSTGRES_USER=questboard_adminand a newPOSTGRES_APP_PASSWORDin.env. - [webapp] Recap-email HTML escaping — GM- and LLM-supplied values
(session_title,campaign_name,summary) are HTML-escaped in recap
emails, and CR/LF are stripped from the subject (header-injection guard). - [webapp] Stop echoing raw LLM errors — the
/askendpoint returns a
genericdetailand logs the real exception server-side. - [bot] HTTP server binds loopback by default —
HTTP_HOSTdefaults to
127.0.0.1(compose overrides to0.0.0.0for the internal network); the
aiohttp request-body cap is now pinned explicitly.
Bug fixes
- [bot]
/record startguard leak — a bad session UUID (or the user not
being in voice) no longer strands the guild in the "starting" set, which had
permanently locked out recording until a bot restart. - [bot] Audio cleanup no longer deletes queued recordings — the
audio_temp
sweep runs once per process (not on every gateway reconnect) and skips any
session directory the bot has marked as handed off to the backend, so
recordings awaiting or undergoing processing survive reconnects and restarts. - [webapp] Duplicate reminder delivery removed — the legacy ETA-scheduled
reminder path is retired; thepoll_session_remindersBeat task (deduped by
session_reminders_sentrows) is the sole delivery mechanism, so a rescheduled
session is reminded exactly once per offset.celery_task_idsis no longer
written. - [webapp] Deterministic lore-match chunk index — replaced
PYTHONHASHSEED-
salted builtinhash()with a SHA-1 digest, so lore consolidation survives a
worker restart mid-pipeline instead of failing with a consolidation timeout. - [bot] Discord embed size budget — a shared
fit_embedhelper keeps the
whole embed (title + description + fields + footer) under Discord's 6000-char
total, so/recapand summary embeds no longer fail outright on long content. - [webapp] Public analytics share page — the frontend now proxies
/public/
to the backend (nginx + Vite dev), so analytics share links load instead of
returning the SPA HTML.
Docs
- Corrected the recording architecture (per-speaker WAV +
/audiopipeline, no
longer "not yet implemented"), the DB-role model, frontend stack versions, and
Python base-image version acrossCLAUDE.md,webapp/CLAUDE.md,
bot/CLAUDE.md,ARCHITECTURE.md,docs/API.md, anddocs/DEVELOPMENT.md.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- [webapp] Discord webhook SSRF allowlist —