fix(installer): validate app archive before deleting installed app on update (#34669)#41643
fix(installer): validate app archive before deleting installed app on update (#34669)#41643DeepDiver1975 wants to merge 1 commit into
Conversation
… update Installer::updateApp() removed the currently-installed app directory (`rmdirr($basedir)`) before copying the new version from the extracted archive, and — unlike installApp() — never checked that the archive actually contained a directory named after the app id. An invalid tarball (top-level directory not matching the app id) therefore deleted the installed app and then tried to copy from a non-existent source, leaving the app broken and surfacing a misleading error instead of a clear message (issue #34669). Mirror installApp()'s guard in updateApp(): compute the expected app directory inside the extract dir and, BEFORE the destructive removal of the installed app, verify it exists. If not, clean up the extract dir and throw the same translated "Archive does not contain a directory named %s" error installApp() uses. The installed app is now left intact on invalid input, and the success path for valid archives is unchanged. Adds a regression test (with an invalid-archive fixture) asserting the clear error is thrown and the previously-installed app is not deleted. Fixes #34669 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
|
Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a changelog item based on your changes. |
DeepDiver1975
left a comment
There was a problem hiding this comment.
🤖 Automated code review by Claude Code review agent
Overview
The change mirrors installApp()'s "archive contains the expected app directory" guard into updateApp(), and — the important part — places it before the destructive OC_Helper::rmdirr($basedir). An app archive whose top-level directory does not match the app id now throws a clear, translated "Archive does not contain a directory named %s" error and leaves the installed app intact, instead of deleting it and copying from a non-existent source. Net: 66/-4 across Installer.php, a new regression test, and a binary fixture.
Correctness
- Guard ordering is correct. The
if (!\is_dir($appInExtractDir))check is now emitted beforeif (\is_dir($basedir)) { rmdirr($basedir); }. The destructive removal can no longer run for an invalid archive. ✔ - No path mismatch. The guard tests the exact same
$appInExtractDirvariable that the subsequentOC_Helper::copyr($appInExtractDir, $basedir)consumes — guard and copy source are guaranteed identical. ✔ $lis properly obtained.$l = \OC::$server->getL10N('lib');is added at the top ofupdateApp(), consistent withinstallApp()/downloadApp()/checkAppsIntegrity()in the same file. ✔- Valid-archive success path unchanged. For a well-formed archive
$appInExtractDirexists, the new branch is skipped, and the original remove→copy→cleanup→OC_App::updateApp()flow runs exactly as before. ✔ checkAppsIntegrity()does not interfere. It takes$extractDirby value (no&), so its internal$extractDir .= '/'.$folder(descending intowrongname/) is local; the$extractDirseen byupdateApp()remains the temp-folder root. Thus$appInExtractDir = <root>/testappcorrectly does NOT exist (onlywrongname/does) and the guard fires. ✔- Minor improvement over
installApp(): the new guard uses\is_dir(...)whereasinstallApp()uses\file_exists(...).is_diris marginally stricter (rejects a same-named regular file) and is the right predicate here. Harmless divergence.
Tests
Traced testUpdateWithInvalidArchiveKeepsInstalledApp() end to end against the fixture (wrongname/appinfo/info.xml, <id>testapp</id>, <version>0.9</version>, identical to the valid testapp.zip apart from the top-level dir name):
installApp()succeeds (id resolved from info.xml =testapp);isInstalled()true;getAppPath('testapp')returns the on-disk path (resolved by filesystem presence, independent of enabled state). ✔updateApp(invalid):checkAppsIntegrity()returnsid=testappcleanly —isAppCompatiblepasses (same content as the valid app used by the existing green tests) and the version-mismatch check is skipped becauseappdatacarries noversion. So no earlier/different exception is thrown that would defeatassertStringStartsWith('Archive does not contain a directory named', …). The guard's exception is the one observed. ✔- Post-failure
assertTrue(\is_dir($appPath))confirms the installed app directory survives the failed update — the regression assertion that matters. ✔
The test follows existing file conventions (getTemporaryFile('.zip'), OC_Helper::copyr, appdata id=testapp/level=100), matching testUpdateIntoWritableAppDir(). I expect it to pass in CI. The fixture is a valid 4-entry zip with the documented structure. ✔
Minor
- The
$thrownflag + try/catch pattern works but@expectedException/expectException()is the more idiomatic PHPUnit form. Not blocking — the manual pattern is needed here to also assert post-state after the throw, so it is justified. - Inline-comment and message wording deliberately match
installApp(); good for consistency.
Verdict
approve — No blocking issues. The guard is correctly ordered before the destructive removal, uses the same path as the copy, and the regression test genuinely exercises and would pass. This is a clean, faithful backport of the installApp() safeguard and closes a real data-loss path in updateApp().
Summary
Fixes #34669 — updating an app with an invalid tarball produced a misleading error and could destroy the already-installed app.
Installer::updateApp()did this:Unlike
installApp()— which validates that the extracted archive contains a directory named after the app id before copying —updateApp()had no such guard. So an archive whose top-level directory does not match the app id caused the installed app to be deleted and then copied from a non-existent source, leaving the app broken and surfacing a confusing error instead of a clear one.Change
Mirror
installApp()'s guard insideupdateApp(), and crucially run it before the destructive removal:$appInExtractDir) up front.installApp()uses:"Archive does not contain a directory named %s".Result:
Tests
Adds
InstallerTest::testUpdateWithInvalidArchiveKeepsInstalledApp()plus a fixturetests/data/testapp_invalid.zip(top-level dirwrongname,info.xmlidtestapp). It installs the valid app, attempts an update with the invalid archive, and asserts (a) the"Archive does not contain a directory named …"exception is thrown and (b) the previously-installed app directory still exists.🤖 This PR was prepared by the Claude Code review agent from the analysis of #34669. Please review carefully before merging.