From 0563757ea43b853770305f80c763a547525abf66 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 11 Jul 2024 20:53:37 +0200 Subject: [PATCH] fix(SetupCheck): Properly check public access to data directory MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit When checking for public (web) access to the data directory the status is not enough as you might have a webserver that forwards to e.g. a login page. So instead check that the content of the file matches. For this the `.ncdata` file (renamed from `.ocdata`¹) has minimal text content to allow checking. ¹The file was renamed from the legacy `.ocdata`, there is a repair step to remove the old one. Signed-off-by: Ferdinand Thiessen --- .../SetupChecks/DataDirectoryProtected.php | 16 +++++++--- .../DataDirectoryProtectedTest.php | 20 ++++++------ lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Repair.php | 2 ++ .../Repair/NC30/RemoveLegacyDatadirFile.php | 32 +++++++++++++++++++ lib/private/Setup.php | 7 ++-- lib/private/Updater.php | 7 ++-- lib/private/User/Manager.php | 2 +- lib/private/legacy/OC_Util.php | 8 ++--- tests/lib/UtilCheckServerTest.php | 18 +++++------ tests/lib/UtilTest.php | 2 +- 12 files changed, 84 insertions(+), 32 deletions(-) create mode 100644 lib/private/Repair/NC30/RemoveLegacyDatadirFile.php diff --git a/apps/settings/lib/SetupChecks/DataDirectoryProtected.php b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php index 5afdfaaddd5..447c66d5c5d 100644 --- a/apps/settings/lib/SetupChecks/DataDirectoryProtected.php +++ b/apps/settings/lib/SetupChecks/DataDirectoryProtected.php @@ -42,13 +42,21 @@ class DataDirectoryProtected implements ISetupCheck { public function run(): SetupResult { $datadir = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValue('datadirectory', '')); - $dataUrl = $this->urlGenerator->getWebroot() . '/' . $datadir . '/.ocdata'; + $dataUrl = $this->urlGenerator->getWebroot() . '/' . $datadir . '/.ncdata'; $noResponse = true; - foreach ($this->runHEAD($dataUrl, httpErrors:false) as $response) { + foreach ($this->runRequest('GET', $dataUrl, [ 'httpErrors' => false ]) as $response) { $noResponse = false; - if ($response->getStatusCode() === 200) { - return SetupResult::error($this->l10n->t('Your data directory and files are probably accessible from the internet. The .htaccess file is not working. It is strongly recommended that you configure your web server so that the data directory is no longer accessible, or move the data directory outside the web server document root.')); + if ($response->getStatusCode() < 400) { + // Read the response body + $body = $response->getBody(); + if (is_resource($body)) { + $body = stream_get_contents($body, 64); + } + + if (str_contains($body, '# Nextcloud data directory')) { + return SetupResult::error($this->l10n->t('Your data directory and files are probably accessible from the internet. The .htaccess file is not working. It is strongly recommended that you configure your web server so that the data directory is no longer accessible, or move the data directory outside the web server document root.')); + } } else { $this->logger->debug('[expected] Could not access data directory from outside.', ['url' => $dataUrl]); } diff --git a/apps/settings/tests/SetupChecks/DataDirectoryProtectedTest.php b/apps/settings/tests/SetupChecks/DataDirectoryProtectedTest.php index 9925ce12c44..fbee52fcf0f 100644 --- a/apps/settings/tests/SetupChecks/DataDirectoryProtectedTest.php +++ b/apps/settings/tests/SetupChecks/DataDirectoryProtectedTest.php @@ -45,7 +45,7 @@ class DataDirectoryProtectedTest extends TestCase { $this->logger = $this->createMock(LoggerInterface::class); $this->setupcheck = $this->getMockBuilder(DataDirectoryProtected::class) - ->onlyMethods(['runHEAD']) + ->onlyMethods(['runRequest']) ->setConstructorArgs([ $this->l10n, $this->config, @@ -59,16 +59,17 @@ class DataDirectoryProtectedTest extends TestCase { /** * @dataProvider dataTestStatusCode */ - public function testStatusCode(array $status, string $expected): void { - $responses = array_map(function ($state) { + public function testStatusCode(array $status, string $expected, bool $hasBody): void { + $responses = array_map(function ($state) use ($hasBody) { $response = $this->createMock(IResponse::class); $response->expects($this->any())->method('getStatusCode')->willReturn($state); + $response->expects(($this->atMost(1)))->method('getBody')->willReturn($hasBody ? '# Nextcloud data directory' : 'something else'); return $response; }, $status); $this->setupcheck ->expects($this->once()) - ->method('runHEAD') + ->method('runRequest') ->will($this->generate($responses)); $this->config @@ -82,10 +83,11 @@ class DataDirectoryProtectedTest extends TestCase { public function dataTestStatusCode(): array { return [ - 'success: forbidden access' => [[403], SetupResult::SUCCESS], - 'error: can access' => [[200], SetupResult::ERROR], - 'error: one forbidden one can access' => [[403, 200], SetupResult::ERROR], - 'warning: connection issue' => [[], SetupResult::WARNING], + 'success: forbidden access' => [[403], SetupResult::SUCCESS, true], + 'success: forbidden access with redirect' => [[200], SetupResult::SUCCESS, false], + 'error: can access' => [[200], SetupResult::ERROR, true], + 'error: one forbidden one can access' => [[403, 200], SetupResult::ERROR, true], + 'warning: connection issue' => [[], SetupResult::WARNING, true], ]; } @@ -95,7 +97,7 @@ class DataDirectoryProtectedTest extends TestCase { $this->setupcheck ->expects($this->once()) - ->method('runHEAD') + ->method('runRequest') ->will($this->generate([])); $this->config diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 33ee831670e..8a6c6f8ccc2 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1793,6 +1793,7 @@ return array( 'OC\\Repair\\NC22\\LookupServerSendCheck' => $baseDir . '/lib/private/Repair/NC22/LookupServerSendCheck.php', 'OC\\Repair\\NC24\\AddTokenCleanupJob' => $baseDir . '/lib/private/Repair/NC24/AddTokenCleanupJob.php', 'OC\\Repair\\NC25\\AddMissingSecretJob' => $baseDir . '/lib/private/Repair/NC25/AddMissingSecretJob.php', + 'OC\\Repair\\NC30\\RemoveLegacyDatadirFile' => $baseDir . '/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php', 'OC\\Repair\\OldGroupMembershipShares' => $baseDir . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\CleanPreviews' => $baseDir . '/lib/private/Repair/Owncloud/CleanPreviews.php', 'OC\\Repair\\Owncloud\\CleanPreviewsBackgroundJob' => $baseDir . '/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 4d0981f7fe8..3b758ff71fe 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1826,6 +1826,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Repair\\NC22\\LookupServerSendCheck' => __DIR__ . '/../../..' . '/lib/private/Repair/NC22/LookupServerSendCheck.php', 'OC\\Repair\\NC24\\AddTokenCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC24/AddTokenCleanupJob.php', 'OC\\Repair\\NC25\\AddMissingSecretJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC25/AddMissingSecretJob.php', + 'OC\\Repair\\NC30\\RemoveLegacyDatadirFile' => __DIR__ . '/../../..' . '/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php', 'OC\\Repair\\OldGroupMembershipShares' => __DIR__ . '/../../..' . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\CleanPreviews' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/CleanPreviews.php', 'OC\\Repair\\Owncloud\\CleanPreviewsBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php', diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 942cd77e5cb..d1904e08431 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -41,6 +41,7 @@ use OC\Repair\NC21\ValidatePhoneNumber; use OC\Repair\NC22\LookupServerSendCheck; use OC\Repair\NC24\AddTokenCleanupJob; use OC\Repair\NC25\AddMissingSecretJob; +use OC\Repair\NC30\RemoveLegacyDatadirFile; use OC\Repair\OldGroupMembershipShares; use OC\Repair\Owncloud\CleanPreviews; use OC\Repair\Owncloud\DropAccountTermsTable; @@ -187,6 +188,7 @@ class Repair implements IOutput { \OCP\Server::get(AddMetadataGenerationJob::class), \OCP\Server::get(AddAppConfigLazyMigration::class), \OCP\Server::get(RepairLogoDimension::class), + \OCP\Server::get(RemoveLegacyDatadirFile::class), ]; } diff --git a/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php b/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php new file mode 100644 index 00000000000..623163927bd --- /dev/null +++ b/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php @@ -0,0 +1,32 @@ +config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata'; + if (file_exists($ocdata)) { + unlink($ocdata); + } + } +} diff --git a/lib/private/Setup.php b/lib/private/Setup.php index a67d74bd032..62db4879bbc 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -360,9 +360,12 @@ class Setup { Installer::installShippedApps(false, $output); // create empty file in data dir, so we can later find - // out that this is indeed an ownCloud data directory + // out that this is indeed a Nextcloud data directory $this->outputDebug($output, 'Setup data directory'); - file_put_contents($config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', ''); + file_put_contents( + $config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata', + "# Nextcloud data directory\n# Do not change this file", + ); // Update .htaccess files self::updateHtaccess(); diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 6d23e81aa63..e26faf86f92 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -208,9 +208,12 @@ class Updater extends BasicEmitter { } // create empty file in data dir, so we can later find - // out that this is indeed an ownCloud data directory + // out that this is indeed a Nextcloud data directory // (in case it didn't exist before) - file_put_contents($this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', ''); + file_put_contents( + $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata', + "# Nextcloud data directory\n# Do not change this file", + ); // pre-upgrade repairs $repair = \OCP\Server::get(Repair::class); diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 639ce507f4d..2c8cc10dc15 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -783,7 +783,7 @@ class Manager extends PublicEmitter implements IUserManager { '.htaccess', 'files_external', '__groupfolders', - '.ocdata', + '.ncdata', 'owncloud.log', 'nextcloud.log', 'updater.log', diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index d8045e8343d..3b5222fee64 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -687,7 +687,7 @@ class OC_Util { /** * Check that the data directory exists and is valid by - * checking the existence of the ".ocdata" file. + * checking the existence of the ".ncdata" file. * * @param string $dataDirectory data directory path * @return array errors found @@ -701,11 +701,11 @@ class OC_Util { 'hint' => $l->t('Check the value of "datadirectory" in your configuration.') ]; } - if (!file_exists($dataDirectory . '/.ocdata')) { + + if (!file_exists($dataDirectory . '/.ncdata')) { $errors[] = [ 'error' => $l->t('Your data directory is invalid.'), - 'hint' => $l->t('Ensure there is a file called ".ocdata"' . - ' in the root of the data directory.') + 'hint' => $l->t('Ensure there is a file called "%1$s" in the root of the data directory. It should have the content: "%2$s"', ['.ncdata', '# Nextcloud data directory']), ]; } return $errors; diff --git a/tests/lib/UtilCheckServerTest.php b/tests/lib/UtilCheckServerTest.php index f0cc8a57e6f..23da44b6e84 100644 --- a/tests/lib/UtilCheckServerTest.php +++ b/tests/lib/UtilCheckServerTest.php @@ -39,13 +39,13 @@ class UtilCheckServerTest extends \Test\TestCase { $this->datadir = \OC::$server->getTempManager()->getTemporaryFolder(); - file_put_contents($this->datadir . '/.ocdata', ''); + file_put_contents($this->datadir . '/.ncdata', '# Nextcloud data directory'); \OC::$server->getSession()->set('checkServer_succeeded', false); } protected function tearDown(): void { // clean up - @unlink($this->datadir . '/.ocdata'); + @unlink($this->datadir . '/.ncdata'); parent::tearDown(); } @@ -66,9 +66,9 @@ class UtilCheckServerTest extends \Test\TestCase { */ public function testCheckServerSkipDataDirValidityOnSetup() { // simulate old version that didn't have it - unlink($this->datadir . '/.ocdata'); + unlink($this->datadir . '/.ncdata'); - // even though ".ocdata" is missing, the error isn't + // even though ".ncdata" is missing, the error isn't // triggered to allow setup to run $result = \OC_Util::checkServer($this->getConfig([ 'installed' => false @@ -83,7 +83,7 @@ class UtilCheckServerTest extends \Test\TestCase { */ public function testCheckServerSkipDataDirValidityOnUpgrade() { // simulate old version that didn't have it - unlink($this->datadir . '/.ocdata'); + unlink($this->datadir . '/.ncdata'); $session = \OC::$server->getSession(); $oldCurrentVersion = $session->get('OC_Version'); @@ -91,7 +91,7 @@ class UtilCheckServerTest extends \Test\TestCase { // upgrade condition to simulate needUpgrade() === true $session->set('OC_Version', [6, 0, 0, 2]); - // even though ".ocdata" is missing, the error isn't + // even though ".ncdata" is missing, the error isn't // triggered to allow for upgrade $result = \OC_Util::checkServer($this->getConfig([ 'installed' => true, @@ -105,7 +105,7 @@ class UtilCheckServerTest extends \Test\TestCase { /** * Test that checkDataDirectoryValidity returns no error - * when ".ocdata" is present. + * when ".ncdata" is present. */ public function testCheckDataDirValidity() { $result = \OC_Util::checkDataDirectoryValidity($this->datadir); @@ -114,10 +114,10 @@ class UtilCheckServerTest extends \Test\TestCase { /** * Test that checkDataDirectoryValidity and checkServer - * both return an error when ".ocdata" is missing. + * both return an error when ".ncdata" is missing. */ public function testCheckDataDirValidityWhenFileMissing() { - unlink($this->datadir . '/.ocdata'); + unlink($this->datadir . '/.ncdata'); $result = \OC_Util::checkDataDirectoryValidity($this->datadir); $this->assertEquals(1, count($result)); diff --git a/tests/lib/UtilTest.php b/tests/lib/UtilTest.php index a83f27d5cf7..cef3f4c063d 100644 --- a/tests/lib/UtilTest.php +++ b/tests/lib/UtilTest.php @@ -162,7 +162,7 @@ class UtilTest extends \Test\TestCase { public function testCheckDataDirectoryValidity() { $dataDir = \OC::$server->getTempManager()->getTemporaryFolder(); - touch($dataDir . '/.ocdata'); + touch($dataDir . '/.ncdata'); $errors = \OC_Util::checkDataDirectoryValidity($dataDir); $this->assertEmpty($errors); \OCP\Files::rmdirr($dataDir); -- 2.39.5