ANetBBS Changelog
Current release: v1.1.53 (October 2026). This file covers v1.0.0
onward, which follows standard semantic versioning — patch releases are
v1.0.1, v1.0.2, and so on. The full internal beta build-number
history (v1.0a1.1 through v1.0b2.239) that got the project to this
release is preserved in
CHANGELOG-beta.md.
v1.0.48 — Live "X just logged in/out" presence alerts (August 2026)
Classic multi-node BBS behavior: every other currently-online user now
sees a real-time alert when someone logs in or logs out, wherever
they currently are on the BBS (a menu, a board, chat) — not just
inside a dedicated chat room. Works across every front-end and every
combination of them: a telnet login is seen live by users on SSH,
rlogin, or the web, and a web login is seen live by every terminal
user, and vice versa for logouts. Nobody is alerted about their own
login or logout.
Terminal and web run in separate processes in a real deployment, so
this required a small real-time relay between them rather than
piggybacking on a single in-process mechanism — see
models.PresenceEvent's docstring for the design.
Every part of this has a dedicated regression test, including the
actual production watchdog code that prints the alert into an active
terminal session (not a re-implementation of its logic). Full test
suite green.
v1.0.47 — MSP/federation registry security audit; three new themes (August 2026)
A full security and hardening pass over the MSP inter-BBS instant
messaging subsystem and the federation registry (never previously
targeted by a dedicated audit): tightened authentication on the
registry's hub-side API, closed several outbound and inbound
network-request validation gaps, added rate limiting and connection
bounds to the inbound MSP listener, and added a scheduled cleanup job
for stale registry entries. The federation registry's join-request
notification now also emails the hub sysop directly (previously
in-app only), and the Inter-BBS Instant Messages inbox's unread-row
styling is now theme-aware instead of a fixed color pair that could
clash with some themes.
Also: three new site themes — Graphite Teal and Ivory Editorial
(a dark and a light professional option, each with its own type
pairing), and Retro Web '99 (a tiled-background, beveled,
Windows-95-era pastiche, for fun). Also fixed: a pre-existing bug on
the homepage where the Recent Posts and Message Boards lists had a
hardcoded color style that ignored the active theme entirely, and a
low-contrast admin dropdown menu specific to Retro Web '99.
Every fix in this pass has a dedicated regression test. Full test
suite green.
v1.0.46 — ANetBBS Pulse: a read-only mobile status dashboard (August 2026)
A new admin-only status dashboard at /admin/pulse/, built for a phone:
live callers (terminal + web), per-service health with CPU/RAM pulled
from the existing metrics sampler, disk and host uptime, and 24-hour
activity totals, auto-refreshing every 15 seconds. Installable to a
phone's home screen as a standalone app on both Android and iOS.
Read-only by design. No service-control actions, no shell, no arbitrary
log access, and no unauthenticated status endpoint — every route
requires an authenticated admin account through the same gate already
used across the rest of the admin surface. API responses are marked
private, no-store; caller IP and peer addresses are never included in
the payload. The installable service worker caches only the dashboard's
own static assets and explicitly refuses to cache anything under
/admin/. Each data section degrades independently on a database
hiccup instead of failing the whole dashboard.
v1.0.45 — Real BinkP outbound bundle compression and WaZOO FREQ support; a full anetbbs-cfg audit (August 2026)
Closes the two documented "Known BinkP limitations" from docs/06-echomail.md.
Outbound bundle compression — outbound .pkt bundles can now be
sent as real ZIP-compressed ArcMail bundles, per-hub/per-node, off by
default. Turn it on from Admin → Echomail / Hub Management, from
anetbbs-cfg, or remotely as a downstream node's own sysop via the
standard AreaFix %COMPRESS GZIP / %COMPRESS OFF command (previously
parsed but a documented no-op). Bundle naming verified against the
real, published FTS-0006 "WaZOO Filename Conventions" document and
Synchronet's own reference docs — day-of-week extension (.Mo0, etc.),
never .zip.
WaZOO file requests (FREQ) — ANetBBS can now both answer a peer's
FREQ and send its own, following the real FTS-0006 convention. This
turned out to be a different, older mechanism than BinkP's M_GET
command (which is spec'd only for resuming an in-progress transfer,
never for requesting an arbitrary file) — verified against the
published FTS-0006 document rather than assumed. Answering is opt-in
per file area (Admin → File Areas → "Allow FREQ", optional password);
sending is queued from a new Admin → Echomail → WaZOO FREQ page.
Matched files are delivered via the existing TIC-push queue on the
requester's next poll, not synchronously within the same session — a
deliberate scope choice (see anetbbs/echomail/freq.py's docstring),
not full same-session Multiple-Batch-mode.
A full audit of anetbbs-cfg (the SSH/console terminal config
tool), requested directly, plus its first dedicated documentation page
(docs/28-anetbbs-cfg.md, all 16 sections covered in detail). Found
and fixed real gaps accumulated across several releases: the System
section's .env editor was missing MSP, MRC bridge, the file-upload
moderation queue, casino starting balances, the wiki edit gate, and QWK
hub identity fields entirely; File Areas was missing the new FREQ
toggle. Also found and fixed a real bug reaching beyond anetbbs-cfg
itself: builtin_python (the game type ANetCRAFT actually uses) was
missing from both anetbbs-cfg's and the web admin's own game-type
choice list, meaning no admin surface at all could create a new
builtin_python game through its form.
v1.0.44 — Full security/dependency/docs audit: two real path-traversal bugs fixed, dependency floors brought current, a dead config setting wired up (August 2026)
A full sweep across security, dependency freshness, and documentation accuracy, requested directly. Three parallel reviews (dependency/CVE audit against every pinned floor in requirements.txt, a code security review of everything shipped since the last audit plus a fresh broad sweep, and a docs-freshness sweep across the rest of docs/), with every finding independently re-verified before being acted on — CVE claims checked live against OSV.dev, code findings checked by reading the actual source, before any fix was written.
Security fixes:
- Two real path-traversal bugs, both a missing-path-separator bug in a directory-confinement check (str(path).startswith(str(root)) instead of startswith(str(root) + os.sep)) — a sibling directory whose name happens to start with the same prefix (e.g. a gallery at .../nasa and an unrelated .../nasa-secret) passed the check. anetbbs/web/gallery.py's image route was reachable by any logged-in user for .zip-gallery entries (the one file type that bypasses Werkzeug's own separately-safe send_from_directory); anetbbs/web/gallery_admin.py's file-delete route had the identical bug, admin-gated. anetbbs/web/file_areas.py's equivalent route already did this correctly, confirming these were a regression rather than an accepted pattern. Both fixed to match the correct existing pattern; regression tests confirm both are exploitable pre-fix and blocked post-fix.
- Ebook reader's text-fetch shelled out to curl with an unsanitized third-party-API-supplied URL (anetbbs/web/ebooks.py) — no scheme check, no private-address check, and no -- end-of-options guard against an option-like URL. Now validated the same way every other attacker-influenceable fetch target in this app is (core/net_safety.resolve_safe_destination, the same helper the RSS poller's SSRF guard already used), plus a -- separator.
- No rate limiting on netmail's "Crash" immediate dial-out (anetbbs/web/netmail.py) — ticking Crash (or replying to a crash-delivered netmail) spawns a background thread that dials out over BinkP immediately, bypassing the normal poll schedule; unlike every other route that can trigger repeated outbound network activity, compose() had no rate limit at all. Now capped at 20/5min per user, matching the established pattern already used elsewhere (file uploads).
Dependency floors brought current in requirements.txt — several had drifted stale since the last audit, including two that were themselves inside a newly-disclosed vulnerable range: cryptography (44.0.1 → 48.0.1, was in-range for CVE-2026-69247) and urllib3 (2.2.2 → 2.7.0, was in-range for CVE-2026-44431). Also bumped: Flask (3.1.3, CVE-2025-47278), aiohttp (3.14.3, CVE-2026-69243/69244), Pillow (12.3.0, ~2 major versions stale, CVE-2026-59199/55798), requests (2.33.0, CVE-2026-25645). Werkzeug gained its own explicit CVE-floor comment and version pin (3.1.6) for the first time — it previously had none at all, sitting directly under a comment that actually covered a different package (Jinja2).
A real, previously-silent config gap fixed: FILE_MOD_QUEUE_ENABLED (upload moderation queue) was documented in docs/07-file-areas.md and docs/11-spam-control.md as a working .env setting, but anetbbs/config.py never defined it — setting it and restarting did nothing. Wired into Config, admin.py's live-editable settings list, and both .env.example files. Fixing this surfaced a second, related bug: the setting takes effect without a restart, meaning the admin Settings page can write the raw string 'false' into the live config — and bool('false') is True in Python. Both read sites in file_areas.py now parse via str(...).lower() == 'true' instead.
Documentation fixes: README's Game Center list was missing ANetDarkForces (a real, shipped 21st game) and gave the wrong path for the terminal gallery viewer (claimed /home/<user>/, actually $INSTALL_DIR/); docs/17-development.md's door-types table was missing builtin_python (the exact precedent doc readers need for a from-scratch bundled Python door); docs/SECURITY.md dropped a stale "this is the alpha" framing left over from before the project reached its current stable v1.0.x line.
v1.0.43 — Third-party license audit: accurate, complete attribution for the Synchronet compatibility layer and LORD (August 2026)
A full audit of every third-party file bundled with ANetBBS's Synchronet door-game support (anetbbs/games/sbbs_stubs/, anetbbs/games/sbbs_reference/, and the pre-installed LORD door), and a rewritten NOTICE file reflecting exactly what was found. No code changes — this is a licensing/attribution correctness pass, prompted by a closer read of Synchronet's own published copyright terms (GPLv2, with named LGPL exceptions that don't apply here).
- GPLv2 attribution corrected and completed. 8 files already carried an intact Synchronet GPLv2 header; roughly 60 more carry a Synchronet CVS revision tag proving the same origin but had lost their license text somewhere along the way — these are now explicitly documented as GPLv2 too, rather than left unaddressed.
cardlib.js's own separate third-party GPLv2 grant (Randolph E. Sommerfeld) is called out distinctly. The actual GPLv2 license text is now bundled atanetbbs/games/sbbs_stubs/LICENSE-GPL2— previously referenced by nothing in the repo. - Every other identifiable license correctly attributed: MIT (
string.js,jpath.js,qrcode.js), Creative Commons Attribution 3.0 (sha1.js), Creative Commons Attribution-ShareAlike 4.0 (openweathermap.js), and Public Domain (json2.js, embedded insbbs_reference/synchronet-json.js). - The remainder — files with a named community contributor but no formal license statement, and files with no header at all — are now honestly documented as such, rather than silently uncategorized, with an invitation for any author to get in touch.
- LORD's own provenance is documented for the first time: original design by Seth Able Robinson, JavaScript port by Stephen Hurd ("Deuce"), per an in-game credits screen — informally licensed per decades of BBS door-game community practice, not under any OSI or GPL license as bundled. Also flags that
pickle/LICENSE(one of LORD's optional add-ons) is an unfilled GPLv3 template, not a completed license grant. README.mdandLICENSEnow both point toNOTICEfor the full breakdown.
v1.0.42 — A genuine no-root install path, documented and verified end to end (August 2026)
New: a fully documented, verified no-root installation path (docs/01b-no-root-install.md) for anyone without sudo/root access. The capability mostly already existed in anetbbs-install but was undocumented and had one real bug — its final instructions always said sudo systemctl start ... even after declining systemd entirely. Added a third, genuinely rootless service option (systemd user units, persisting via loginctl enable-linger), fixed the final instructions to match whichever choice was made, and fixed a real live bug found while proving this end to end: MSP/SYSTAT default to enabled and were never addressed by the wizard, so every install logged a permission-denied bind failure on every boot — now correctly disabled by default. Verified with a real install run as an ordinary non-root user, followed by actually booting the result and confirming clean HTTP 200s with zero permission errors. 22 new tests.
v1.0.41 — Fixed a real bug where the MRC bridge always showed blank BBS info to the wider network; manual install docs now cover MRC setup (August 2026)
Fixed a real bug where every BBS running the MRC bridge showed blank telnet/SSH/website/sysop/description info to the wider MRC network — the default outcome for every install, not an edge case. install.sh/update.sh wrote the wrong config key names and never wrote telnet/SSH contact info at all; fixed in both, with real telnet/SSH strings now derived at install time. Existing installs need their mrc/bridge/config.json corrected by hand (neither script overwrites an existing one) — see docs/INSTALL.md's troubleshooting section. 12 new tests.
The manual install guide (docs/INSTALL.md) now covers MRC bridge setup, the one real gap found in a full audit of the doc against current code (everything else already checked out).
v1.0.40 — Fixed a live BinkP process-corruption bug; hub node polling and message flavor (Crash/Hold) now do what they say (August 2026)
Fixed a real production incident where BinkP delivery silently stopped working after running clean for days. The in-app notification helper's live-toast push imported web_app on the fly to reach its SocketIO instance — safe from the real web process (already running under eventlet from the start), but anetbbs-binkp.service is a plain asyncio process that was never meant to load eventlet at all. The first time an inbound BinkP session delivered netmail to a real local user, that import triggered eventlet.monkey_patch() mid-process, silently corrupting already-created threading primitives and breaking the SQLAlchemy connection pool for the rest of that process's life (RuntimeError: cannot notify on un-acquired lock on every DB write afterward) — so outbound TIC/echomail delivery on later sessions from that same process quietly stopped working, looking like a routing problem rather than what it actually was. Fixed by checking current_app.extensions instead of importing web_app fresh — the same safe pattern already used elsewhere in the codebase for this exact class of bug. Reproduced the production traceback exactly by reverting the fix locally before confirming it, and added a regression test that does the same.
Hub-initiated polling of downstream BinkP nodes can now run on a real schedule, not just manually. Each node can now be given its own auto-poll interval on its admin page; leaving it blank keeps the node exactly as it works today (manual/poll-in-only).
Crash and Hold now actually change delivery timing, not just a cosmetic packet flag. Crash now fires an immediate out-of-schedule delivery attempt the moment a netmail or file-echo item is queued; Hold now actually excludes an item from our own outbound dial-out, shipping only once that peer polls in to us. File-echo/TIC distribution gets these flags for the first time, exposed as Crash/Hold checkboxes on file-area uploads.
New: inline in-place animation for sysop-defined ANSI/CP437 screens. Any screen shown at login/logoff/etc. can now embed a short looping animation directly in its body (@ANIMSTART@...@FRAME@...@ANIMEND@ markers, alongside the existing @PAUSE@ pagination convention) instead of only ever being static art. No code changes needed to use it — drop an animated screen into data/mods/text/ the same way any other custom screen override already works. Documented in docs/04-ansi-screens.md and the in-app wiki.
Fixed a real live bug where a SAUCE-tagged .ans file showed its own metadata as literal garbage text on screen — including the main menu's own file-based art override, and every welcome/goodbye/newuser/custom screen. SAUCE stripping previously only existed inline inside the ANSI Editor's import route; now a single shared sauce.strip() is used everywhere a raw .ans/.asc file is read from disk.
Over 2,780 tests, all green.
v1.0.39 — Second, deeper security and performance audit pass, including docs (August 2026)
A follow-up audit was carried out across the whole codebase, documentation, and deployment tooling — broader in scope than v1.0.38's pass, this round also covered the wiki feature and every doc file, not just application code and scripts. Additional real issues turned up and were fixed, again addressed in strict severity order: further hardening of network-facing protocol handlers, closing a couple more resource-leak and unbounded-growth points in long-running background processes, tightening several more file-permission and credential-handling spots in the install/update/container tooling, adding a few more missing rate limits and size caps, and correcting some troubleshooting documentation that could otherwise lead a sysop toward a weaker configuration than intended. New regression tests were added throughout for every fix — the full suite now sits at over 2,750 tests, all green. As with the previous round, this entry intentionally omits specifics in the interest of responsible disclosure; sysops running an older release should update at their earliest convenience.
v1.0.38 — Full security and performance audit, hardening pass across the codebase (August 2026)
A comprehensive, self-directed security and performance audit was carried out across the whole codebase, documentation, and deployment scripts — covering the web application, terminal/telnet/SSH session handling, echomail (FidoNet BinkP/QWK), MRC chat, door-game launching, and the install/update tooling. A number of real issues turned up and were fixed, addressed in order of severity: hardening of remote/cross-network-facing attack surfaces, closing several memory- and resource-leak points in long-running processes, tightening file and credential permissions across the install/update/backup scripts, adding missing resource limits and concurrency caps, and bumping several third-party dependencies to their current patched releases (see requirements.txt). New regression tests were added throughout — the suite now sits at over 2,600 tests, all green. In keeping with responsible disclosure, this entry intentionally omits specifics; sysops running an older release should update at their earliest convenience.
v1.0.37 — install.sh/update.sh portability audit: real fixes for Alpine, Pi, and minimal/Docker hosts (August 2026)
A sysop report that visudo rejected deploy/sudoers.anetbbs on a host whose sudo build has no I/O-logging support (an "unknown setting: log_input" error) prompted a full portability audit of both install.sh and update.sh against the stated goal of working on any distro, including Raspberry Pi and Docker. Several real, independently-confirmed bugs were found and fixed:
- Sudoers refresh no longer fails outright on sudo builds without I/O-logging support. Both scripts now retry once with the offending
!log_input, !log_outputDefaults lines stripped if the full file failsvisudo -cf, rather than leaving a stale sudoers file in place (which had been silently missing whatever commands changed since the sysop's last successful refresh). install.sh --uninstallran without checking it was root, letting every destructive command fail silently while still reporting a clean "uninstalled" success.install.sh --defaults(the documented non-interactive install path) could still block on an interactive prompt and auto-abort on any OS not in the package-manager detection list — exactly the situation a non-interactive install (Docker build, CI) is most likely to hit.- Both scripts checked only that the
systemctlbinary exists, not that systemd is actually running as PID 1. Some Docker images have the binary layered in without real systemd, causing both scripts to fail opaquely partway through instead of failing fast with a clear message — more dangerous inupdate.sh, which actively stops the live BBS mid-upgrade with no other restart path. update.sh's main file-sync step had no error checking at all, printing "Files synced" unconditionally even ifrsyncfailed — meaning a failure there could leave the database migrated against stale, un-synced application code. It now aborts cleanly and restarts whatever it had stopped.update.sh's database-migration fallback (used whensudoitself is missing) silently skipped all column-migration logic, reporting "Database schema updated" while actually adding no new columns. It now runs the same real migration as the primary path.- Fixed several GNU-coreutils-only constructs (
grep -oP,head -n -N) that silently break on BusyBox/Alpine, and a missingfilecommand dependency that silently defeated the ARM/x86 binary-architecture safety check for bundled doors. - Raspberry Pi's older
ID=raspbianreleases, and other unlisted distro derivatives (via/etc/os-release'sID_LIKE), are now correctly detected instead of falling through to "unknown package manager." - Added full Alpine (
apk) package-manager support toinstall.sh— detection, package installation, and per-package name mapping, plus installing Alpine'sshadowpackage so the existing user/group-management commands work unchanged. Note: Alpine natively uses OpenRC rather than systemd, and both scripts still require systemd, so this support currently only applies to an Alpine host that also runs systemd.
docker/ (Dockerfile, entrypoint scripts, compose file, single-container supervisord config) was also audited separately and found to already be in good shape, with no changes needed.
v1.0.36 — New data/mods/ sysop-override tree, matching Synchronet's own mods/ convention (August 2026)
Added a central data/mods/ directory where a sysop can drop a customized replacement for anything ANetBBS ships, guaranteed to survive a package update untouched — the same guarantee real Synchronet's own /sbbs/mods/ tree gives (wiki.synchro.net/dir:mods), and built to the same unified layout: one root directory whose subdirectories mirror what's being overridden, not a scattered set of one-off override points.
Three things are covered:
- Synchronet-compat door/game scripts.
synchronet_compat.py's shim already computed amods_dirvalue and threaded it through asbbs.mods_dir/system.mods_dir(real Synchronet API properties some doors read directly), but the internaljs.mods_dirits ownload()resolver needed to actually use that value never existed — so nothing ever consulted it. Fixed:js.mods_dirnow exists and is checked first inload()'s search order, anddoor_runner.pyapplies the same override to a door's own top-level entry-point script (the one file that never goes throughload()at all, since it's handed directly to jsexec/Node as the process entry point). - ANSI/menu screen overrides.
data/mods/text/anddata/mods/text/menus/are now checked ahead of the existingdata/text/anddata/text/menus/override locations for welcome/goodbye/newuser/custom ANSI screens and built-in terminal menu art — matching real Synchronet's ownmods/text//mods/text/menu/convention. The olderdata/text/locations keep working unchanged for anyone already using them;data/mods/text/is simply preferred when both exist. See doc 4 — ANSI screens and doc 14 — door games. - ANetBBS's own native core Python screens. Real Synchronet's
login.js/logon.jsare core system scripts (not doors) a sysop can customize inmods/the same way as anything else, because Synchronet's whole engine loads everything by filename. ANetBBS's own core isn't script-driven the same way, so a matching capability needed a new, explicit mechanism:core/mods_override.py'scall_core_override()checksdata/mods/core/<name>.pyfor a full replacement Python file and falls back to the built-in version if it's missing, broken, or throws — never breaking the screen it's overriding. See doc 14 — door games.
data/mods/ (and its text//text/menus//core/ subdirectories) sits inside the part of the install update.sh already excludes wholesale, so a sysop's dropped-in override survives a package update with zero extra deploy-script changes.
v1.0.35 — Network-join credentials email now tracked, with a resend button (August 2026)
"The last two people said they have not received [the join-approval credentials email]... should have a resend feature." Real gap: the auto-send at approval time was always a one-shot, best-effort attempt with NOTHING persisted about whether it actually succeeded — just a flash message that's gone on the next page load, and a log line. If the sysop approved several requests in a row (or just didn't catch the banner), a silent SMTP-side failure (relay bounce, greylisting, quota) for one specific applicant was completely invisible and unrecoverable short of grepping the app log.
Added three tracking columns to NetworkJoinRequest (email_sent_at, email_last_attempt_at, email_error) and a shared _send_join_approval_email() helper used by both the automatic send-on-approval and a new Resend button (on both the Join Requests list and each request's detail page) — so the two paths can never drift apart on wording, and delivery status is now visible at a glance instead of relying on catching a transient banner. Resend deliberately re-sends the same already-generated credentials rather than regenerating a new password, since regenerating would silently invalidate whatever the applicant may have already received or configured their own mailer with from an earlier, partially-successful attempt.
v1.0.34 — New BinkP outbound spool directory; fixed a redelivered-TIC sysop confusion (August 2026)
Added a real BinkP outbound spool directory. Real gap found live: ANetBBS's own echomail has always been entirely DB-queue-driven — outbound netmail/echomail as EchomailMessage rows, outbound file distribution as HatchQueue rows — with no way at all for an external program (e.g. a door that writes its own FTS-0001 netmail packets straight to disk, the same way any traditional FTN mailer's flat-file outbound spool works) to hand ANetBBS a file to transmit. Any file dropped in a peer's spool directory is now sent as-is on the next BinkP session with that peer (dial-out via poller.py, dial-in via binkp_server.py — both directions covered) and archived to a sent/ subfolder on success, never deleted outright. Spool directories are keyed per peer (not one shared folder like the inbound directory), since a loose file has no address of its own to route by and more than one network/node can be configured at once — <DATA_DIR>/binkp/outbound/<peer address>, overridable via BINKP_OUTBOUND_DIR. The resolved path for each configured peer is now shown directly on the Echomail Networks list and a BinkP node's own detail page, so a sysop doesn't have to read source to find it.
Fixed a real live bug: a redelivered TIC file that was already successfully filed weeks earlier looked stuck, but "Rescan Inbound Now" reported nothing was wrong. Root cause, traced from a real report of a .tic/binary pair sitting in the inbound directory that the rescan button claimed didn't exist: some file echoes periodically redistribute already-delivered files unchanged, and scan_inbound()'s dedup-skip branch (correctly refusing to re-file something already marked filed in the DB) did a bare continue without ever reaching the cleanup step that moves a processed file out of the inbound directory — so a genuine redelivery piled up in inbound forever, invisible to the rescan button's "no unprocessed .tic files found" message. Fixed by extracting the existing cleanup logic into a shared _move_to_processed() helper and calling it from the dedup-skip path too, with the rescan count now reflecting files it swept.