Skip to content

[2.x] Harden the updater: authenticate it, and pause the queue while it runs - #4930

Merged
imorland merged 2 commits into
2.xfrom
im/harden-updater-and-pause-queue
Aug 12, 2026
Merged

[2.x] Harden the updater: authenticate it, and pause the queue while it runs#4930
imorland merged 2 commits into
2.xfrom
im/harden-updater-and-pause-queue

Conversation

@imorland

@imorland imorland commented Aug 12, 2026

Copy link
Copy Markdown
Member

Fixes #0000

Changes proposed in this pull request:

Hardens the updater — the page shown while the code is ahead of the database, between uploading new files and running migrations. Two things were wrong with that window.

Authentication. The updater ran pending migrations for anyone who submitted the right database.password, but it only checked the password when one was set. An install with no database password — which the installer produces, and which is valid for socket-auth MySQL/MariaDB/Postgres — had no check at all: a bodyless POST ran every pending migration. Now the username and password are both verified for those drivers, so a passwordless install is still gated by the username (which the installer always requires). SQLite has neither, so it falls back to the database file's name. The comparison uses hash_equals, and every failure returns the same generic error.

The queue. Nothing stopped a queue worker running jobs against a schema that was mid-migration. The queue is now paused while migrations run and resumed when they finish. The web updater arms the pause the moment it first serves the update page — the danger starts when the version drifts, not when an admin clicks Update. php flarum migrate (run any time from the CLI) pauses around its own run too, but only when something is actually pending, so a routine no-op migrate doesn't idle every worker. Resume always runs in a finally, so a failed migration never leaves the queue stuck. This uses the existing queue:pause/queue:resume mechanism — not maintenance mode, so the forum stays online to visitors.

Impacted: UpdateController, IndexController and the updater form; MigrateCommand; InstalledApp.

Reviewers should focus on:

  • The SQLite fallback is a weak secret. flarum.sqlite is guessable. The case for it: SQLite is a supported production driver, and leaving one driver with zero check while the others have one is hard to defend. It raises the bar from nothing to "must know the db name", no more. The root + no-password case stays guessable for the same reason it always was — indefensible from HTTP, and refusing the web updater would break the shared-hosting users it exists for.
  • The queue pause is conditional but the resume is not. The web arm pauses unconditionally when serving the updater (it can't cheaply check pending state on the request path), so MigrateCommand must resume unconditionally to clear it — otherwise a no-op run reached through the updater leaves the queue paused forever. This exact case stuck the queue on my dev forum during development; there's a test for it now.
  • The web arm swallows pause failures (logs, still serves the updater) — a queue that can't be reached must never block the update itself.

Screenshot

Necessity

  • Has the problem that is being solved here been clearly explained?
  • If applicable, have various options for solving this problem been considered?
  • For core PRs, does this need to be in core, or could it be in an extension?
  • Are we willing to maintain this for years / potentially forever?

Confirmed

  • Frontend changes: tested on a local Flarum installation. (updater form)
  • Frontend changes: tests are green — no JS changes
  • Frontend changes: tests have been added — no JS changes
  • Backend changes: tests are green (run composer test).
  • Backend changes: tests have been added, or are not appropriate here.
  • Where applicable, changes are suitable for all supported database drivers (MySQL, MariaDB, PostgreSQL, SQLite).
  • Core developer confirmed locally this works as intended.
  • The description above is written by me and describes what this pull request actually does.

New tests: UpdateControllerTest and IndexControllerTest (unit — verification across all drivers, oracle-free errors, and the form asking for exactly what's checked); MigrateCommandQueuePauseTest and additions to UpgradePageTest (integration — pause/resume, no-op leaves the queue alone, resume-on-failure, the web arm, and the no-op-clears-a-prior-pause regression). All load-bearing behaviours mutation-tested.

Required changes:

  • Related documentation PR: the updater form now asks for username + password (or the database name on SQLite) — the update docs describe the old single-password field.

imorland and others added 2 commits August 12, 2026 23:49
The updater is served while the code is ahead of the database — between
new files landing and their migrations running. Two things were wrong
with that window.

Authentication. The updater ran pending migrations for anyone who
submitted the right database password, but it only checked the password
when one was set. An install with no database password — which the
installer produces, and which is valid for socket-auth MySQL, MariaDB
and Postgres — had no check at all, so a bodyless POST ran every pending
migration. The username and password are now both verified for those
drivers, so a passwordless install is still gated by the username the
installer always requires. SQLite has neither, so it falls back to the
database file's name. The comparison uses hash_equals, and every failure
returns the same generic error so it cannot be used as an oracle.

The queue. Nothing stopped a worker running jobs against a schema that
was mid-migration. The queue is now paused while migrations run and
resumed when they finish. The web updater arms the pause the moment it
first serves the update page — the danger starts when the version
drifts, not when an admin clicks Update. `php flarum migrate`, run at any
time from the CLI, pauses around its own run too, but only when there is
work to do, so a routine no-op migrate does not idle every worker.
Resume always runs in a finally, and unconditionally, so a failed
migration — or a no-op run reached through the updater after the web arm
already paused — never leaves the queue stuck. This uses the existing
queue:pause/queue:resume flag, not maintenance mode, so the forum stays
online to visitors.
@imorland
imorland requested a review from a team as a code owner August 12, 2026 22:50
@imorland imorland changed the title Harden the updater: authenticate it, and pause the queue while it runs [2.x] Harden the updater: authenticate it, and pause the queue while it runs Aug 12, 2026
@imorland imorland added this to the 2.0.0-rc.6 milestone Aug 12, 2026
@imorland
imorland merged commit c6f4134 into 2.x Aug 12, 2026
25 checks passed
@imorland
imorland deleted the im/harden-updater-and-pause-queue branch August 12, 2026 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants