Skip to content

Commit

Permalink
fix(SetupCheck): Properly check public access to data directory
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
susnux committed Aug 8, 2024
1 parent e49c55d commit e396b90
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 31 deletions.
16 changes: 12 additions & 4 deletions apps/settings/lib/SetupChecks/DataDirectoryProtected.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,21 @@ public function getName(): string {
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';

Check notice

Code scanning / Psalm

PossiblyInvalidOperand Note

Cannot concatenate with a array<array-key, string>|string

$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]);
}
Expand Down
20 changes: 11 additions & 9 deletions apps/settings/tests/SetupChecks/DataDirectoryProtectedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected function setUp(): void {
$this->logger = $this->createMock(LoggerInterface::class);

$this->setupcheck = $this->getMockBuilder(DataDirectoryProtected::class)
->onlyMethods(['runHEAD'])
->onlyMethods(['runRequest'])
->setConstructorArgs([
$this->l10n,
$this->config,
Expand All @@ -59,16 +59,17 @@ protected function setUp(): void {
/**
* @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
Expand All @@ -82,10 +83,11 @@ public function testStatusCode(array $status, string $expected): void {

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],
];
}

Expand All @@ -95,7 +97,7 @@ public function testNoResponse(): void {

$this->setupcheck
->expects($this->once())
->method('runHEAD')
->method('runRequest')
->will($this->generate([]));

$this->config
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1793,6 +1793,7 @@
'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',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions lib/private/Repair.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
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;
Expand Down Expand Up @@ -187,6 +188,7 @@ public static function getRepairSteps(): array {
\OCP\Server::get(AddMetadataGenerationJob::class),
\OCP\Server::get(AddAppConfigLazyMigration::class),
\OCP\Server::get(RepairLogoDimension::class),
\OCP\Server::get(RemoveLegacyDatadirFile::class),
];
}

Expand Down
32 changes: 32 additions & 0 deletions lib/private/Repair/NC30/RemoveLegacyDatadirFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Repair\NC30;

use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;

class RemoveLegacyDatadirFile implements IRepairStep {

public function __construct(
private IConfig $config,
) {
}

public function getName(): string {
return 'Remove legacy ".ocdata" file';
}

public function run(IOutput $output): void {
$ocdata = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata';
if (file_exists($ocdata)) {
unlink($ocdata);
}
}
}
7 changes: 5 additions & 2 deletions lib/private/Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,12 @@ public function install(array $options, ?IOutput $output = null): array {
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();
Expand Down
7 changes: 5 additions & 2 deletions lib/private/Updater.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ private function doUpgrade(string $currentVersion, string $installedVersion): vo
}

// 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);
Expand Down
2 changes: 1 addition & 1 deletion lib/private/User/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ private function verifyUid(string $uid, bool $checkDataDirectory = false): bool
'.htaccess',
'files_external',
'__groupfolders',
'.ocdata',
'.ncdata',
'owncloud.log',
'nextcloud.log',
'updater.log',
Expand Down
8 changes: 4 additions & 4 deletions lib/private/legacy/OC_Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ public static function checkDataDirectoryPermissions($dataDirectory) {

/**
* 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
Expand All @@ -701,11 +701,11 @@ public static function checkDataDirectoryValidity($dataDirectory) {
'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;
Expand Down
18 changes: 9 additions & 9 deletions tests/lib/UtilCheckServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ protected function setUp(): void {

$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();
}

Expand All @@ -66,9 +66,9 @@ public function testCheckServer() {
*/
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
Expand All @@ -83,15 +83,15 @@ public function testCheckServerSkipDataDirValidityOnSetup() {
*/
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');

// 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,
Expand All @@ -105,7 +105,7 @@ public function testCheckServerSkipDataDirValidityOnUpgrade() {

/**
* Test that checkDataDirectoryValidity returns no error
* when ".ocdata" is present.
* when ".ncdata" is present.
*/
public function testCheckDataDirValidity() {
$result = \OC_Util::checkDataDirectoryValidity($this->datadir);
Expand All @@ -114,10 +114,10 @@ public function testCheckDataDirValidity() {

/**
* 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));

Expand Down

0 comments on commit e396b90

Please sign in to comment.