Skip to content

WonderCMS 3.7.2

Latest

Choose a tag to compare

@robiso robiso released this 01 Nov 19:06
· 8 commits to main since this release
04afdc6

WonderCMS 3.7.2

Features and fixes

A security release, one day after 3.7.1. It answers a private report about admin sessions, works through the security advisory backlog, and fixes the four small items found right after 3.7.1. Every change carries its own test and was weighed against what WonderCMS is for: small, simple, secure, one admin who owns the site.

Everyone logs in once after this update. Sessions are now tied to the password they were created with, and a session from 3.7.1 or older has no such tie, so it ends at the first request after the update. Log in again and you are done.

Changes

  • Core (index.php)
    • Sessions belong to the password they were created with. Changing the password (in Settings, or by editing the hash in database.js by hand) ends every session created with the old password, the one that changed it included, and a later login does not bring any of them back. The login page then says "Password changed. Log in again." once; a change whose save failed (a full disk) claims nothing and leaves the admin logged in with the old password (PR #417). Before, a session that was open at the time of a password change stayed logged in. Reported privately by Nguyễn Hữu Dũng; thank you (PR #413).
    • The session cookie is sent with HttpOnly, SameSite=Lax, and Secure on HTTPS. On a site that answers on both http and https, a session created over https is not available over http: stay on https, or turn on Force HTTPS in Settings, Security (PR #413).
    • The password hash on disk is kept unless the password change itself is writing it, under a lock, so a save that started before a password change cannot write the old hash back (PR #413). config.forceLogout is no longer read; the key stays in database.js so the file looks the same as before.
    • The install password leaves the public home page at the first login. A new install shows "Your password for editing everything is: ..." on the home page until someone logs in; from 3.7.2 that paragraph, as the installer wrote it, is removed at the first login (and at the next login of a site installed before this check existed, including the older <h4> form from cores before 3.2.0), with one alert: the password was shown publicly until now, please change it in Settings, Security. Nothing is written into the page in its place. A paragraph you edited by hand is yours and is left alone; if your password is still in it, remove it. Reported by pyuysig, cybertron1337 and EQSTLab (GHSA-v8xq-w57m-4v6v; PR #411).
    • Login attempts are throttled per connecting address: after 10 wrong passwords from one address in 15 minutes, the next attempt gets HTTP 429 with a Retry-After header and "Too many attempts. Try again in a few minutes." The password is not checked on that path. Counts live in data/logs/login-attempts.json: a salted hash of each address with the times of its failed attempts, at most 10 per address and 1000 addresses, so the file holds no addresses. IPv6 counts per /64. A fixed list of addresses is never throttled: loopback (127.x, ::1), private (10.x, 172.16 to 172.31, 192.168.x, fc00::/7), link-local (169.254.x, fe80::/10), the shared address space 100.64.x to 100.127.x, the benchmark range 198.18.x to 198.19.x and a few reserved blocks (0.x, 192.0.0.x, 240.x and up, the unspecified address ::). Every other address is throttled, documentation ranges included; a NAT64 address counts as the IPv4 address it carries. The reason: when a proxy on the same machine or network, or a container network, sits in front of the site, every visitor reaches PHP from one such address, and one shared count would let anyone lock the owner out. The consequence: a client that reaches the site directly from the same private network (an office or home LAN install) is not throttled either, as on 3.7.1. Behind a CDN or a proxy on another public address, visitors arrive from that public address, which is throttled like any other: ten wrong passwords through it pause logins through it for up to 15 minutes; deleting data/logs/login-attempts.json clears it. If the counts file cannot be written, nothing is throttled and the login works as before. No cookie, no browser identity. Someone who shares an address with an attacker (the same office network) waits for the attack's duration plus up to 15 minutes; a site that reaches PHP only through a proxy on the same machine or network sees a loopback or private address and gets no throttle, as on 3.7.1. Reported by konukgorkem (GHSA-3ff8-68q8-x7hp) and, independently the same day, by Zachariah Mithani (AploSec); PR #414.
    • Backups are streamed to the admin and nothing stays on the server. The archive is built under data/ with a random name, sent in the same logged-in, token-checked request as a download, and deleted (on a host that disables unlink() it is emptied to zero bytes instead; on one that disables file_put_contents() as well it stays complete under data/, random name, listed nowhere, as in 3.7.1: delete it over FTP). Before, each backup was written into data/files under a date-stamped name with 64 random bits, reachable by URL to anyone who learned the name, and contained every earlier backup. Old backups made by 3.7.1 or earlier stay listed in the Files tab with their link and a note to download and delete them. Reported by HuzaifaDal (GHSA-wxc3-4g29-cp8f; PR #409).
    • A theme or plugin update keeps the installed version until the new one has extracted completely: the installer used to delete the installed folder first and did not check the extract, so a full disk or a bad download during an update left the module missing or cut short while the admin was told it was updated (for Sky, the active theme, that meant a broken site until the folder was uploaded by hand); now a failed extract leaves the installed version in place with an alert (a write cut short by a full disk is caught where the host leaves filesize() enabled), and a non-GitHub archive takes the old path unchanged. An update now needs room for the installed and the new copy at once; one that does not fit is refused with the installed version kept. In the code since 3.3.0 (January 2022; the extract was never checked in earlier versions either); reported by Aurora Rogers (Perch Scan); PR #441.
    • The first-install password is drawn with random_int(), a cryptographic source, instead of str_shuffle() (PR #415). Hardening: the generated password is shown on the home page until the first login anyway. Noted by Zachariah Mithani (AploSec), who also sent a second, independent report of the missing login throttle; thank you.
    • The address in "Last 5 logins" is the connection's own (REMOTE_ADDR, validated), never the Client-IP or X-Forwarded-For request header, and the login hooks receive the same value. A site behind a reverse proxy or CDN sees the proxy's address from the next login; a plugin that wants the forwarded address can read it in the login_success hook. Reported by archnexus707 (GHSA-jjgc-826f-qwgp, whose output half was fixed in 3.7.0; PR #408).
    • Pages: a new or renamed page gets a slug that no menu sibling and no existing page at that level has (-2, -3, ...), so adding a menu page named "404" gives 404-2 instead of overwriting the 404 page, renaming to a sibling's name gives -2, and a rename that keeps the current slug no longer deletes the page. The redirect after a rename goes to the page's real new address (#404, PR #410). Found by the 3.7.1 day-after checks.
    • The login page's "Wrong password." alert is styled on every theme (the admin stylesheet loads only when logged in; a visitor page with an alert now gets the few alert rules inline, scoped so a theme's own .alert boxes are untouched) (#406, PR #412).
    • A search result for a page with subpages ends with /, so relative links on that page resolve the same from search as from the menu (#407, PR #412).
    • The admin's docs links point at https://docs.wondercms.com/ with their anchors (#405, PR #412).
  • Sky theme
    • Sky 3.7.15 (PRs #412, #437): the stylesheet URL carries the file's modification time (or the version where a host disables filemtime()), so a Sky update is seen without a hard refresh, and the phone-width menu panel can be reached with the keyboard. Existing sites update Sky in Settings -> Themes (copy your own edits to Sky first).
  • Project
    • Version bump to 3.7.2 (PR #416). Release PR: #418.
    • From the final review before release: the backup download releases the session lock before streaming, so the admin's other tabs no longer wait (PR #420); the first-login removal of the install password claims success only when the write landed (PR #421); a sent archive is emptied where the host disables unlink() (PR #419); a session ended by a password change or the update is told so once (PR #422); the throttle exempts the explicit list of local, private, link-local, shared-space and benchmark addresses named above (PR #423); two tests made independent of runner speed and of PHP 7.4's php -S (PRs #424, #425). Second pass: the old-backup alert says what is true on every host (PR #426); the backup closes the session before building the archive, so a site whose PHP sessions live inside its folder no longer hangs on Backup (PR #427), and leaves those session files out of the archive (PR #431); the throttle's exempt ranges are written out and identical on every PHP version, with a NAT64 address counted as its embedded IPv4 (PR #428); the sweep leaves an archive alone while it is still being sent (PR #429, completed by #433 below); where the host disables file_put_contents() the throttle adds no fatal before the password check (PR #430). Third pass: the session-file skip covers depth-style save paths and data/ itself, keeps the owner's own files in that folder, and the save-path lookup is silent under open_basedir (PR #432).
    • Fourth pass: the backup sender holds a shared lock on its archive and the sweep leaves alone any archive whose lock would block; where no lock can be taken (the host disables flock() or fopen(), or the filesystem refuses it), a leftover from a killed request waits a day before it is emptied on a host without unlink(); the session-folder match is case-insensitive and an empty save path means PHP's temp folder (PR #433). Fifth pass: where the host disables file_put_contents() nothing fatals any more: the login completes, the admin page renders, and a save fails with the "Could not write" alert (the login's own last-logins save fails quietly; such a host could never save on 3.7.1 either); the sweep locks a read-write handle and skips an archive whose lock would block (where a lock is refused for another reason, or flock() or fopen() is disabled, the age rule applies); the sender's lock is test-pinned (PR #434). Sixth pass: createUniqueSlug() keeps its 3.7.1 public signature (two parameters), so a plugin subclass that overrides it still loads; the rename and parent context moved to a protected helper (PR #436); where the host disables filemtime() the sweep reads an archive's age from stat() and removes nothing when no age is known, and the bundled Sky renders (PR #437, Sky 3.7.15); two more of the #434 guards are test-pinned (PR #435). Seventh pass: the login throttle enforces a count only when it could write it, so a counts file or folder that turned read-only, or a full disk, never refuses the owner (PR #438); menu create and rename call the public createUniqueSlug() again, as 3.7.1 did, so a plugin subclass that overrides it is consulted, and a rename whose slug an override returns for an existing page is refused instead of overwriting it (PR #439); the backup's session close before the archive build is test-pinned (PR #440). Eighth pass: the backup sweep and download work where filesize() is disabled (the size then comes from stat(); where that is disabled too the download goes without a Content-Length header; PR #444); the updater's stale-folder clear and its link guard are test-pinned (PR #442). Two limits of the updater fix stay as they are, on purpose: a custom repository's zip whose top folder is the module's own name (not a GitHub archive) keeps the old order, and a write cut short by a full disk is not measured where the host disables filesize(); the first is not reachable from the official module list and the second needs a host setting no known host uses; a larger fix was reviewed and set aside as out of proportion for WonderCMS.
    • RELEASING.md: step 9 says the demo's themes and plugins are refreshed from the official list after a rebuild (PR #412); step 8 notes that the throttle's counts file holds nothing that identifies a visitor, so no .htaccess edit is asked of older sites (PR #414).

Security advisories

With 3.7.2 live, every report in the GitHub advisory queue has an answer. The ones fixed here are published with credit to their reporters (above). Reports fixed in earlier versions are published as fixed in those versions: manop55555 (GHSA-5x62-xcrh-mhp6 and GHSA-qc3r-v7fc-24xc, fixed in 3.7.0); the second of those was also reported by VulnCheck (Brandon Tarr), who is credited with it. Two reports described actions that need the site's one admin acting on their own site; they were not treated as vulnerabilities, but the paths were hardened and the reporters are credited for that: Saudadeeee (file deletion, hardened in 3.7.0) and aliyevmursal (module upload, hardened in 3.6.0). Reports that describe the admin installing PHP on their own site, or the admin writing HTML into their own pages, are closed as working as designed, each with a reason. Several of these reports waited months for an answer; that was wrong of us, and the answers say so.

Backwards compatibility

  • database.js format unchanged; no method signature or hook of the plugin and theme API changes. One behaviour does, on purpose: a plugin that wrote config.password through set() no longer changes the hash on disk; only the password form does. config.forceLogout is kept but no longer read.
  • Everyone logs in once after the update (see above). Restoring a database.js from before a password change brings back the sessions of that time, since the hash comes back with it.
  • A site served on both http and https: a session created over https is not available over http (the cookie is marked Secure there), and while that https session's cookie exists in the browser a login over http does not stick until the browser session ends. Stay on https, or turn on Force HTTPS.
  • Old backup archives are not deleted for you; the Files tab lists them with a note.
  • The backup is now sent by PHP itself, not by the web server as a file. On a host that caps how long a request may run (IIS FastCGI's default is 90 seconds), a very large backup can stop part-way through the download; the failure is visible in the browser and nothing on the server is changed. Make the backup from a faster connection or raise the host's limit.
  • "Last 5 logins" behind a proxy or CDN shows the proxy's address from now on.
  • Minimum PHP stays 7.4; tested in CI on 7.4, 8.1, 8.3, 8.4 and 8.5.

How to update

  • Log into your WonderCMS website and create a backup through Settings -> Security (it downloads to your computer).
  • Click "Update".
    • If the update is not visible, open Settings -> Themes and click "Check for updates".
  • Log in again (once), then update Sky from Settings -> Themes.

Installation

  • Unzip and upload the files wherever you want WonderCMS installed and visit that URL.
  • SHA-256: 66d1623e4b2358e1160af5939bd08ecfee86ccc253821ed1c8f0d79921ff67ef (wondercms-372.zip, 81165 bytes)