Skip to content

Commit fcfc372

Browse files
DeepDiver1975claude
andcommitted
fix(installer): validate app archive before deleting installed app on 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>
1 parent 4921c51 commit fcfc372

3 files changed

Lines changed: 66 additions & 4 deletions

File tree

lib/private/Installer.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ public static function isInstalled($app) {
214214
* "\OC::$server->getAppConfig()->getValue($appid, 'installed_version')"
215215
*/
216216
public static function updateApp($info= [], $isShipped=false) {
217+
$l = \OC::$server->getL10N('lib');
218+
217219
list($extractDir, $path) = self::downloadApp($info);
218220
$info = self::checkAppsIntegrity($info, $extractDir, $path, $isShipped);
219221

@@ -230,16 +232,25 @@ public static function updateApp($info= [], $isShipped=false) {
230232
$basedir = $currentDir;
231233
}
232234

233-
if (\is_dir($basedir)) {
234-
OC_Helper::rmdirr($basedir);
235-
}
236-
237235
$appInExtractDir = $extractDir;
238236
if (\substr($extractDir, -1) !== '/') {
239237
$appInExtractDir .= '/';
240238
}
241239

242240
$appInExtractDir .= $info['id'];
241+
242+
// Verify the archive actually contains the app directory before
243+
// removing the currently installed app, otherwise an invalid archive
244+
// would leave the user without the previously installed app.
245+
if (!\is_dir($appInExtractDir)) {
246+
OC_Helper::rmdirr($extractDir);
247+
throw new \Exception($l->t("Archive does not contain a directory named %s", $info['id']));
248+
}
249+
250+
if (\is_dir($basedir)) {
251+
OC_Helper::rmdirr($basedir);
252+
}
253+
243254
OC_Helper::copyr($appInExtractDir, $basedir);
244255
OC_Helper::rmdirr($extractDir);
245256

tests/data/testapp_invalid.zip

622 Bytes
Binary file not shown.

tests/lib/InstallerTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,57 @@ public function testUpdateApp() {
9191
$this->assertNotEquals($oldVersionNumber, $newVersionNumber);
9292
}
9393

94+
/**
95+
* Tests that updating with an archive whose top-level directory is not
96+
* named after the app id fails with a clear error message and, crucially,
97+
* does not destroy the already-installed app.
98+
*/
99+
public function testUpdateWithInvalidArchiveKeepsInstalledApp() {
100+
// Install the valid app first
101+
$pathOfOldTestApp = __DIR__ . '/../data/testapp.zip';
102+
$oldTmp = \OC::$server->getTempManager()->getTemporaryFile('.zip');
103+
\OC_Helper::copyr($pathOfOldTestApp, $oldTmp);
104+
$oldData = [
105+
'path' => $oldTmp,
106+
'source' => 'path',
107+
'appdata' => [
108+
'id' => 'testapp',
109+
'level' => 100,
110+
]
111+
];
112+
Installer::installApp($oldData);
113+
$this->assertTrue(Installer::isInstalled(self::$appid));
114+
$appPath = \OC_App::getAppPath(self::$appid);
115+
$this->assertNotFalse($appPath);
116+
$this->assertTrue(\is_dir($appPath));
117+
118+
// Attempt to update with an invalid archive: its top-level directory is
119+
// "wrongname" although info.xml declares the id "testapp".
120+
$pathOfInvalidTestApp = __DIR__ . '/../data/testapp_invalid.zip';
121+
$invalidTmp = \OC::$server->getTempManager()->getTemporaryFile('.zip');
122+
\OC_Helper::copyr($pathOfInvalidTestApp, $invalidTmp);
123+
$invalidData = [
124+
'path' => $invalidTmp,
125+
'source' => 'path',
126+
'appdata' => [
127+
'id' => 'testapp',
128+
'level' => 100,
129+
]
130+
];
131+
132+
$thrown = false;
133+
try {
134+
Installer::updateApp($invalidData);
135+
} catch (\Exception $e) {
136+
$thrown = true;
137+
$this->assertStringStartsWith('Archive does not contain a directory named', $e->getMessage());
138+
}
139+
$this->assertTrue($thrown, 'updateApp() should throw for an invalid archive');
140+
141+
// The previously installed app must still be present and untouched.
142+
$this->assertTrue(\is_dir($appPath), 'The installed app directory must not be deleted on invalid update');
143+
}
144+
94145
/**
95146
* Tests that update is installed into writable app dir if the original app dir is not writable
96147
*/

0 commit comments

Comments
 (0)