v2.5.4
·
122 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Added
cms.updates.composer_binaryconfig key (#232) — priority-1 override for the self-updater's composer binary path, populated by default fromenv('COMPOSER_BINARY'). SettingCOMPOSER_BINARY=/opt/homebrew/bin/composerin a Laravel.envfile now works from HTTP-request context, matching the advice thecomposerBinaryNotFounderror message gives.CMS_PHP_BINARYenv var + CLI PHP interpreter resolution for composer (follow-up to #225) — a newresolvePhpBinary()walks CLI-shaped candidates (Herd, Homebrew, system) and rejects anything whose basename smells like-fpm/-cgi.CMS_PHP_BINARYwins when set; CLI SAPI short-circuits toPHP_BINARY. The resolved CLI PHP is used to invoke composer during both install and the rollback pre-check.
Fixed
- Updater metadata GETs blocked by userland HTTP listeners (#231) — the feed check, single-release lookup, SHA-256 sidecar fetch, and custom JSON endpoint all went through Laravel's
Httpfacade, which dispatchesRequestSending/ResponseReceivedaround every request. Herd Pro'sHttpClientWatcher, Telescope, Debugbar, and custom monitoring listeners could block or corrupt the request lifecycle — when Herd's dump-server socket was wedged, the check died aftermax_execution_timeinstead of the ~200ms round-trip. A newMetadataClientstatic helper uses a rawGuzzleHttp\Clientand bypasses Laravel's HTTP factory event dispatch entirely; retry semantics (5xx retry, config-driven attempt count) are preserved for the custom JSON endpoint. The download path still usesHttp::sink()(already shielded by #224). COMPOSER_BINARYin.envdidn't take effect under PHP-FPM (#232) —ApplicationUpdateManager::envComposerBinary()read viagetenv('COMPOSER_BINARY'), but Laravel 11+'s default dotenv adapter populatesenv()/$_ENVwithout callingputenv(), sogetenv()returnedfalsefrom HTTP-request context and the priority-1 override was silently skipped.envComposerBinary()now readscms.updates.composer_binary(populated fromenv('COMPOSER_BINARY')in the shipped config) first, then falls back togetenv()for hosts that exportCOMPOSER_BINARYat the OS level. ThecomposerBinaryNotFounderror message now names both the.env/config path and the OS-level path so operators aren't sent down a broken workaround.- Composer discovery failures were undiagnosable (#233) —
discoverComposerBinary()now collectsis_file()/is_executable()per candidate path and emits a structuredLog::warningon failure so operators can distinguish a wrong-path failure from a PHP-FPM sandboxed-stat failure (macOS Herd Pro sandboxes/opt/homebrew/*; chrooted FPM pools and restrictiveopen_basedirbehave the same way). The warning includesphp_sapiand a hint pointing at the fix.UpdateException::composerBinaryNotFound()accepts either the legacy flat string list or the new per-candidate diagnostic shape and renders the stat outcome for each path in the exception message, so operators don't have to check the log. Legacy callers still work; a mixed-shape input degrades to path-only rendering instead of raising aTypeError. - PHP-FPM interpreter used to invoke composer (follow-up to #225) — under PHP-FPM,
PHP_BINARYpoints at the FPM daemon binary, which prints usage and exits 64 when handed the composer PHAR. That produced the misleading"Composer install failed. Output: ."symptom on Herd hosts — composer was discovered fine, but the interpreter used to invoke it was the wrong SAPI. Rollback then wrongly reported"Composer binary could not be located"becauseverifyComposerBinaryAvailable()reused the same broken interpreter.buildComposerCommand()and the rollback pre-check now use the resolved CLI PHP; a newcomposerVerificationFailedexception surfaces the resolved binary, PHP interpreter, exit code, and captured output when--versionfails, so future misdiagnoses don't repeat. - Silent extraction failures left partial installs on disk (#236) — inside
ApplicationUpdateManager::extractUpdate()'s per-entry loop, a failedfopen('wb')orfread()on the target file previously didcontinue/breakwithout logging or throwing. Extraction of that entry was silently abandoned, nothing propagated toperformUpdate()'s catch block, andhandleUpdateFailure()/rollback never triggered — leaving a partial install on disk that failed to boot on the next request with no obvious cause. Failures are now logged with the entry + errno and thrown via a newUpdateException::extractionEntryFailed()factory soperformUpdate()'s catch block rolls back to the pre-update snapshot.
Security
- Fail closed when update source omits SHA-256 checksum (#235) —
ApplicationUpdateManager::maybeVerifyChecksum()previously logged a warning and returned silently when the update source did not advertise a SHA-256 hash, letting the updater download and execute arbitrary remote code without integrity verification. That fail-open behavior also weakened the reachability story for #234.maybeVerifyChecksum()now throwsUpdateException::checksumRequired()when no checksum is advertised. An explicit opt-in —cms.updates.allow_unverified_updates(defaultfalse, envCMS_UPDATES_ALLOW_UNVERIFIED) — is available for hosts on trusted networks or air-gapped mirrors that intentionally accept the risk; those hosts still get the original warning log. - Zip-slip in
extractUpdate()(#234) —ApplicationUpdateManager::extractUpdate()built the write target by concatenatingbase_path()with an unvalidated entry name from the ZIP, then streamed viafopen()/fwrite(). Because this bypassedZipArchive::extractTo(), PHP's own traversal mitigations did not apply, so a crafted archive entry likerelease-root/../../../etc/cron.d/xwould write outside the install root once the common prefix was stripped. Entries whose normalized path starts with/or contains..segments are now rejected before the target directory is created, andrealpath()verifies the resolved parent still sits under the extraction root before opening the write stream. Both failures are logged and the entry is skipped.