Releases: Panxatony/Saito
Release list
Saito 8.4.12 "obenauf"
[8.4.12] - 2026-08-17 "obenauf"
No migration. One file changed.
php bin/cake.php schema_cache clear-
✓ Fixed: the message 8.4.11 added never reached the web server's log. It
was chained to the original exception, and PHP prints a chained exception
previous-first — so the entry began with the library's own trace, and the web
server truncated the line before the useful half. Tried on the test install:
the operator saw exactly what they saw before 8.4.11.It is thrown unchained now and leads the entry. The library's wording is
repeated inside it, so only its internal frames are lost — which were never
the part worth reading.This is the second correction to the same fix in one evening, and both came
from running it rather than reasoning about it. 8.4.11 was verified through
the command line, where PHP prints the whole chain and nothing is truncated;
the web path, which is the one an operator actually meets, behaved
differently.
Saito 8.4.11 "beipackzettel"
[8.4.11] - 2026-08-17 "beipackzettel"
No migration. One file changed.
php bin/cake.php schema_cache clearWarning
Before upgrading, check config/.env for values containing a space:
grep -nE '=[^"'"'"']*[[:space:]]' config/.envAnything that matches needs quotes — export NAME="two words". This applies
to 8.4.10 as well; the release below only makes the failure legible.
-
✓ Fixed: an unreadable crash when
config/.envholds an unquoted value with
a space. The reader introduced in 8.4.10 refusesNAME=macnemo Forumwhere
the previous one accepted it — and it refuses it during bootstrap, before
CakePHP has an error page. What an operator got was HTTP 500 and a stack trace
naming neither the file nor the remedy.This is not hypothetical and was not caught by the migration's own testing:
the production install went down for five minutes on the day 8.4.10 shipped,
on a value written years earlier and read without complaint ever since. The
parser comparison behind that release checkedNAME="two words"— quoted —
and never tried it without the quotes, which is the likelier way to write it.The failure remains a failure: continuing without the file would swap a clear
stop for a confusing database error later. What changes is that the log now
names the file, repeats the value the library objected to, and says what fixes
it. Three tests hold down that the reader really does refuse this input and
that its own words do not tell anybody what to do — the two facts the wrapped
message rests on.docs/update.mdanddocs/upgrade.mdcarry the check to run before
upgrading, which is the only place it helps.
Saito 8.4.10 "nachschub"
[8.4.10] - 2026-08-17 "nachschub"
No migration. The JS bundle changed with the Alpine.js update, so the
compiled assets go out with it.
php bin/cake.php schema_cache clearTwo suppliers had stopped producing — one nine years ago, one three — and this
release replaces them. A third was kept, deliberately, because it still delivers
what it is asked for.
-
Δ Changed: the RSS feeds are written with
laminas/laminas-feed.
suin/php-rss-writerlast published in 2017 and still declaresphp >=5.4.0.
Nothing was wrong with it; it simply stopped.The gate was not that the feed still validates but that the
guidmust not
move — it is what subscribers are keyed on, and changing it makes every
reader re-announce every posting it has already shown. Compared before and
after across both feeds, against real postings and not only test data:guid,
pubDate,linkand the posting bodies are byte-identical. Nobody's reader
will report anything as new.Two faults the old library had been swallowing came out in the process, and
either would have replaced the whole document with an error rather than
degrading quietly: an empty subject, which replies commonly have, and an empty
body. Both are now handled.Visible differences are equivalent or better: item titles are escaped instead
of CDATA-wrapped,atom:linkis absolute where it used to be a bare path a
reader cannot resolve, and the channel carries a description instead of an
empty element.<generator>names the forum without a version rather than
announcing the library, and the writer'sslash:comments— which would have
told every reader that every posting has no replies — is taken back out. -
Δ Changed:
config/.envis read withvlucas/phpdotenv.
josegonzalez/dotenvlast published in 2023 and sits onm1/env, silent since
2020 — two dormant layers.symfony/dotenvwas the first choice and was rejected after measuring: it
interpolates the unbraced$nameform as well, so a value containing a dollar
sign — a database password, most plausibly — is silently rewritten, and
truncated where the name is undefined.phpdotenvreads every case tried
exactly as before.A key defined both in the environment and in
config/.envno longer
errors. The old loader raised "Key already defined" and answered with an
HTTP 500; now the environment simply wins. If you removed a key from one side
to work around that, you can put it back.One narrower change:
\$inconfig/.envnow resolves to a literal$
rather than staying two characters. A password written that way changes
meaning. Bare$nameand${name}behave as before. See
update.md. -
✓ Fixed: three places said the
.envloader had to be switched on. It
does not —config/bootstrap.phpreads the file whenever it exists, and has
for a long time.docs/deployment-debian.md, the header of
config/.env.default(which also misspelledbootstrap.php) and the comment
the other two were copied from now describe what actually happens, including
that the environment wins twice over:APP_NAMEpresent skips the file
entirely, and otherwise only unset keys are filled in. -
Δ Changed: Alpine.js 3.15.12 → 3.16.1, and the build tooling (
cssnano,
eslint,globals,typescript-eslint) to current.
Saito 8.4.9 "altschlüssel"
[8.4.9] - 2026-08-16 "altschlüssel"
No migration. One PHP file changed — no assets, no schema.
php bin/cake.php schema_cache clear-
✓ Fixed: a remember-me cookie that can never work again is now taken back.
The token in that cookie has had three parts since 7.0.4 — account, expiry,
signature. One with two parts is the shape the authentication library used
before that, and it is refused, because its second part is a password hash and
it carries no expiry at all. Refusing it was never the problem.Nothing cleared it. The browser presented the same dead cookie on every
request that followed, so the member was returned to the login form over and
over — every fifteen minutes in the case that surfaced this, second factor and
all — and could do nothing about it, because the cookie isHttpOnlyand
therefore invisible and untouchable from their side. Anyone who last ticked
"stay signed in" before 2026-06-30 can be carrying one.Only shapes that cannot work are taken back. A current three-part token that
fails on its expiry or its signature is left alone: judging that belongs to
the authenticator, and such a cookie carries a real lifetime and leaves on its
own.The discard is written to
saito-info.log. The silence is what made this
expensive to find: when the cookie is refused no authenticator succeeds, so
what reaches the application is the form authenticator complaining that some
unrelated address "did not match/login" — a message pointing away from the
cause. An evening went into session timeouts, the reverse proxy and the
two-factor tables before the cookie itself came into view.What an operator will see: a member in this state is asked to log in one
last time, and from then on "stay signed in" holds. Nobody who was working
normally is affected — a valid cookie is never touched.
Saito 8.4.8 "weissblende"
[8.4.8] - 2026-08-16 "weissblende"
No migration. One stylesheet per macnemo preset changed; nothing else.
php bin/cake.php schema_cache clear-
✓ Fixed: the primary button reads white again. Bootstrap 5 works out a
button's label colour from its background rather than always using white, and
on macnemo's orange it picked black — so a button that had looked one way for
years changed with 8.4.7 and looked wrong.Only the label is set back. The orange is untouched: it carries white at
3.21:1, above the 3:1 the accessibility guidelines ask of large text, which a
bold button label is. Bootstrap was applying the stricter body-text threshold,
which is a defensible choice rather than a bug — just not the right one for a
button.
Saito 8.4.7 "unterbau"
[8.4.7] - 2026-08-16 "unterbau"
No migration. The stylesheets and the JS bundle changed, so those go out
with it.
php bin/cake.php schema_cache clearNothing here is meant to be seen. The rooms look the same; what carries them has
been replaced — Bootstrap, the web fonts, the admin area's form rendering. The
one visible change is a number on a phone.
-
Δ Changed: the themes run on Bootstrap 5.3.8. 4.6.2 had had no
maintenance since January 2023, and its JavaScript was never loaded here, so
this was a maintenance question rather than a security one — but an
unmaintained dependency is exactly what this line of releases is for.A major version does move things: spacing, corner radii and form controls all
shift a little. Nothing was held to a pixel comparison, deliberately — that
gate is what turns a migration into a reimplementation.Bootstrap 5.3 also brings a colour-mode implementation built on custom
properties, which the hand-maintained night presets can build on later. -
Δ Changed: the stylesheets have a spacing and text scale of their own.
Seventy-three@extendonto Bootstrap's utility classes became real
declarations against$space-xs…$space-xl,$text-quietand friends.
@extend .mb-3said "whatever Bootstrap means by three"; a theme could
neither reason about that nor override it. Documented in
customizing.md. -
Δ Changed: the theme fonts come from
@fontsource. Thetypeface-*
packages stopped publishing in 2022. Cabin and Fenix are unchanged — same
faces, same licence, a maintained package. -
− Removed: the BootstrapUI dependency. It had not been doing anything
since the CakePHP 5 upgrade: it supplied its helpers through
Controller::$helpers, a property CakePHP 5 removed, so the admin area had
been rendering with the framework's own FormHelper for some time without
anyone noticing. Removing it changes nothing that is rendered — verified
byte-for-byte across thirteen admin pages. -
✓ Fixed: the number of members online now shows on a phone. It is
anchored to the widget's icon, and that icon is hidden on narrow screens — so
the count went with it, and the folded state, the one where the figure is
worth having, was the one state it never appeared in.
Saito 8.4.6 "dienstschlüssel"
[8.4.6] - 2026-08-15 "dienstschlüssel"
One migration runs, and it rewrites entries. Not additive like the five
before it: nine columns change type, so the table is rebuilt row by row.
Measured between 70 seconds and four and a half minutes for 680,000 postings,
depending on the server. On MariaDB 11.2 or later the forum stays writable while
it happens; everywhere else the table is readable but not writable until it
finishes. Run it from the command line — PHP's execution limit will cut a
rewrite short through the web updater.
php bin/cake.php migrations migrate
php bin/cake.php schema_cache clearThe key that comes with the office. A forum can now require a second factor of
the people who can change it — and, because a lock without a key is how an
operator loses their own forum, three ways back out.
-
+ Added: a forum can require two-factor authentication of its moderators
and administrators.offby default, so upgrading changes nothing for
anybody; an operator sets it tomodoradminin the admin area. Ordinary
members are never caught by it — the cost of a compromised member account is
one member, the cost of a compromised administrator account is the forum.Every exemption in it is load-bearing. Somebody who switches the setting on
while their authenticator app is on a phone in another room must still be able
to set the second factor up, and to log out; both stay reachable from behind
the gate, and both are tested one by one. For the case that has no interface
left at all there isbin/cake two_factor_reset <username>, which clears the
second factor, the recovery codes, the trusted devices and the passkeys of one
named account without touching the account itself. -
+ Added: the columns postings are written to now outlive 2038.
timestamp
cannot hold an instant after 2038-01-19; nine columns move todatetime,
which has no such limit, anduseronline.time— a Unix timestamp in a signed
INT, the same date by another road — becomesBIGINT.Eleven years off, and worth doing now rather than under pressure: the
conversion has to rebuild the tables either way, and the two ways it can go
wrong quietly are both handled. The session's timezone decides what a
timestampbecomes when it turns into adatetime, so the migration pins it
to UTC rather than trusting the host's clock; and a0000-00-00left over
from an older MySQL would abort the whole chain under that server's default
sql_mode, so those values are let through unchanged instead. -
✓ Fixed: the new setting showed its own translation key where its name and
explanation belong. The admin area reads those from thenondynamiccatalogue
and the strings had been added todefault, so the screen offered
2fa_required_from_role_expas if it were a sentence. A test now renders every
settings label and fails on any key that reaches the page untranslated. -
✓ Fixed: the passkey is mentioned where people look for it. It could only
be found by opening the second-factor screen and reading to the end, which is
not where somebody wonders whether their fingerprint would work here. -
Δ Changed: the upgrade and update documents describe what this migration costs
and which releases need the command line, and the bot-filter section explains
the exemption static files need — a challenged stylesheet arrives as
Content-Type: text/htmland the forum renders unstyled, with nothing in the
log to find.
Saito 8.4.5 "handschlag"
[8.4.5] - 2026-08-07 "handschlag"
Two migrations run. One adds webauthn_credentials; the other deletes
credentials belonging to accounts that no longer exist, and touches nothing that
belongs to a live one. Neither has a down(). The stylesheets and the JS bundle
both changed, so those go out with it.
php bin/cake.php migrations migrate
php bin/cake.php schema_cache clearA handshake instead of six digits: the device you are holding confirms the login
itself. And the release the account deletion should always have been — asking to
be erased now erases everything.
-
+ Added: passkeys as a second factor. Touch ID, Face ID, Windows Hello or
a hardware key instead of typing the code. The operating system checks the
fingerprint or the face on the device and hands the forum a signature; no
biometric reaches the server, and none can.An addition, never a replacement, in three ways that all matter. A passkey can
only be registered once the code is already on, because it lives in one
machine's secure enclave and the recovery codes that come with the code are
the way back from a lost device. The code field stays exactly where it was, so
a browser without JavaScript or a device without a sensor reaches the same
second step. And the button stays hidden until the browser confirms an
authenticator exists, because one that cannot work is worse than none.Two things surprise people once and then never again, so they are in the help:
a passkey belongs to the device it was made on — register each one you use —
and it works for one address only, which is exactly why a fake site cannot
harvest it. -
✓ Fixed: deleting an account now deletes what it could be signed in with.
It did not. Measured before the fix, a deleted member left behind an encrypted
second-factor secret, ten hashed recovery codes, a trusted-device token and a
passkey — thirteen rows for an account that no longer existed, plus any
outstanding password-reset token.The five tables arrived one per release through the 8.4 line, each built and
tested on its own, and none of those test runs asked what happens when the
account goes. It matters most against the reason the data export already
exists: this forum implements the GDPR's right of access as self-service, and
leaving credentials behind after an erasure request does not sit beside that.The migration clears what has already accumulated. A fix that only applied
from today would leave exactly the people who already exercised that right
still on file. -
Δ Changed: the API scope is decided by one test instead of two that could
disagree./api/v2chooses JWT-only authentication, and the check for it was
an unanchored match: any path merely containing the string picked it, so
/entries/view/1/api/v2would have shown a signed-in member as logged out.
Stricter rather than looser, so never an escalation — but the CSRF exemption
beside it has always been an anchored prefix, and a loose half of a pair
invites harmonising them in the wrong direction. Both now use the same test.
Saito 8.4.4 "stammgast"
[8.4.4] - 2026-08-07 "stammgast"
One migration runs. It adds two_factor_trusted_devices; there is no
down(). The stylesheets changed — Bota, Nova and Macnemo, all three — so
those have to go out with it. The JS bundles did not.
php bin/cake.php migrations migrate
php bin/cake.php schema_cache clearA regular is somebody the house recognises on the way in. Turning on the second
factor in 8.4.2 quietly took "stay signed in" away, and this puts it back for
devices that have actually proved themselves.
-
✓ Fixed: "stay signed in" works again with a second factor. 8.4.2 shipped
it broken in two ways at once, and the note in that release — that a
remember-me cookie is refused outright for an enrolled account — described the
second of them as if it were the whole design. It was half a design.The checkbox sits on the password form, but the cookie it asks for can only be
minted a step later, once the code is in, and nothing carried the answer
across: no remember-me cookie was ever written for an enrolled account. Even
had one been, it was turned away at the door. The result was that switching on
2FA meant signing in again and again — most visibly on a phone, where the
browser drops sessions freely.The reasoning behind the refusal still holds: a remember-me cookie is
stateless, validating against a username and a password hash, so a cookie made
before an account enrolled cannot be told from a later one and no server can
revoke either. What was missing was somewhere to write down which is which.two_factor_trusted_devicesis that place. A row is written only after a
second factor has actually been proved, and its token travels in a companion
cookie; a remember-me cookie is honoured for an enrolled account only when a
matching, unexpired row sits behind it. Cookies from before enrolment have no
row and are still refused, so nothing that was closed has been reopened.Because the trust is a row now rather than a signature, it can be taken back.
Signing out drops the device doing it and leaves the others alone; switching
the second factor off, changing the password, or an administrator resetting it
drops every one. The token is stored as a SHA-256, so reading the table yields
nothing that could be put in a cookie, and the cookie itself carries the same
flags as the remember-me cookie it travels with. -
✓ Fixed: code was unreadable inside an alert in every dark theme. The base
stylesheet givescodethe page's text colour, which is near-white in a dark
preset; Bootstrap's contextual alerts keep their light backgrounds there,
deliberately. Neither is wrong alone — the base rule reaching inside the alert
is. Measured, the two-factor recovery codes came out at about 1.2:1 on pale
green, which is not "hard to read" but absent, and they are shown exactly once
with no second chance. Inside an alert, code now takes the alert's own colour.The recovery codes also had a class with no styles behind it, and now read as
something meant to be copied off a screen: monospace, tracked, with room
between the lines. -
Δ Changed:
league/commonmarkis updated to 2.9.0. Six advisories were
published against it an hour before this release was built — five denial of
service, one bypassing theAttributesExtensionfilter onhrefandsrc—
and the release refused to package over them.None of them is reachable from the forum: markdown is parsed only for the help
pages and the guided tour, which are files shipped with the release, while
postings go through Saito's own parser. So this is hygiene rather than an
incident. It still blocks a release, because knowingly shipping a dependency
an hour after it was declared vulnerable is not a thing to do.
Saito 8.4.2 "zweitschlüssel"
[8.4.2] - 2026-08-06 "zweitschlüssel"
One migration runs. It adds two_factor_credentials and
two_factor_recovery_codes; there is no down(). Neither the stylesheets nor
the JS bundles changed, so those can stay as they are.
php bin/cake.php migrations migrate
php bin/cake.php schema_cache clearA second key to the same door. Members can now protect their account with a
code from an authenticator app on top of the password — off by default, and
entirely their own choice.
Nothing changes for anybody who does not switch it on.
-
+ Added: two-factor authentication (TOTP). Set it up in your profile: the
forum shows a QR code, your app produces a six-digit code, and only once you
have proved you can produce one does the second factor go live. Ten single-use
recovery codes come with it, shown once, for the day the phone is gone.From then on the login has two steps. The second one happens inside the login
overlay rather than on a page of its own, because it is a second step, not a
second destination.The part that matters and cannot be seen: the password alone no longer
signs anybody in. It is verified, and then nothing happens — no identity, no
session, and above all no "remember me" cookie, which is minted at exactly the
moment 2FA has not reached yet. A remember-me cookie made before an account
enrolled is refused outright: it validates against a username and a password
hash, so no server can revoke it, and the only way to stop it walking past the
second factor is to turn it away at the door.Secrets are encrypted at rest, recovery codes are hashed like passwords, and
neither table is mass-assignable. Turning the second factor off, or minting
fresh recovery codes, asks for the password again — a borrowed session should
not be able to do either quietly.Lost the phone and the codes? An administrator can clear the second factor
from the user list. It asks for the administrator's own password and is logged
either way, successful or not: removing somebody's second factor is exactly
the step an attacker who reached an admin session would take, so it leaves a
trace that can be read afterwards. -
✓ Fixed: the whole-forum export was written world-readable. It holds every
member's e-mail address, and their IP addresses where the forum stores them,
and the default umask made it0644— on a shared host, the membership list
for any local account. It is0600in a0700directory now, and the command
refuses to write rather than write unprotected. -
Δ Changed: the nightly security job also checks that the security headers
still arrive. Advisories were the only thing it watched, but server
configuration drifts too and far more quietly: one installation was serving
every static file and every upload withoutX-Content-Type-Options, and
another had no HSTS at all. Nothing fails when that happens — pages render,
uploads load — so it took a scheduled check to notice. Three probes per
installation, because nginx applies headers per location and locations drift
apart independently. -
Δ Changed: a command-injection advisory in
squizlabs/php_codesnifferis
patched (dev-only, never runs on a server), and the one third-party GitHub
Action is pinned to a commit rather than a tag its owner could repoint.