Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(SetupCheck): Properly check public access to data directory #46456

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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
2 changes: 1 addition & 1 deletion tests/lib/UtilTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public function testNeedUpgradeCore() {

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);
Expand Down
Loading