diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 17e47f62a7d8b..3c2e7a3e625f1 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -83,6 +83,7 @@ 'OCA\\Settings\\SetupChecks\\CheckServerResponseTrait' => $baseDir . '/../lib/SetupChecks/CheckServerResponseTrait.php', 'OCA\\Settings\\SetupChecks\\CheckUserCertificates' => $baseDir . '/../lib/SetupChecks/CheckUserCertificates.php', 'OCA\\Settings\\SetupChecks\\CodeIntegrity' => $baseDir . '/../lib/SetupChecks/CodeIntegrity.php', + 'OCA\\Settings\\SetupChecks\\ConfigValidation' => $baseDir . '/../lib/SetupChecks/ConfigValidation.php', 'OCA\\Settings\\SetupChecks\\CronErrors' => $baseDir . '/../lib/SetupChecks/CronErrors.php', 'OCA\\Settings\\SetupChecks\\CronInfo' => $baseDir . '/../lib/SetupChecks/CronInfo.php', 'OCA\\Settings\\SetupChecks\\DataDirectoryProtected' => $baseDir . '/../lib/SetupChecks/DataDirectoryProtected.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index 1dccc69b923a5..03cc3ca52c4c8 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -98,6 +98,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\SetupChecks\\CheckServerResponseTrait' => __DIR__ . '/..' . '/../lib/SetupChecks/CheckServerResponseTrait.php', 'OCA\\Settings\\SetupChecks\\CheckUserCertificates' => __DIR__ . '/..' . '/../lib/SetupChecks/CheckUserCertificates.php', 'OCA\\Settings\\SetupChecks\\CodeIntegrity' => __DIR__ . '/..' . '/../lib/SetupChecks/CodeIntegrity.php', + 'OCA\\Settings\\SetupChecks\\ConfigValidation' => __DIR__ . '/..' . '/../lib/SetupChecks/ConfigValidation.php', 'OCA\\Settings\\SetupChecks\\CronErrors' => __DIR__ . '/..' . '/../lib/SetupChecks/CronErrors.php', 'OCA\\Settings\\SetupChecks\\CronInfo' => __DIR__ . '/..' . '/../lib/SetupChecks/CronInfo.php', 'OCA\\Settings\\SetupChecks\\DataDirectoryProtected' => __DIR__ . '/..' . '/../lib/SetupChecks/DataDirectoryProtected.php', diff --git a/apps/settings/composer/composer/installed.php b/apps/settings/composer/composer/installed.php index d2b87e1bdfdff..fc450b459ab6c 100644 --- a/apps/settings/composer/composer/installed.php +++ b/apps/settings/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', + 'reference' => '7bc4ccba6ad85c31a1e500023a9514059d584548', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e', + 'reference' => '7bc4ccba6ad85c31a1e500023a9514059d584548', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), diff --git a/apps/settings/lib/AppInfo/Application.php b/apps/settings/lib/AppInfo/Application.php index 9f7ec3036f43f..0fadb44add237 100644 --- a/apps/settings/lib/AppInfo/Application.php +++ b/apps/settings/lib/AppInfo/Application.php @@ -52,6 +52,7 @@ use OCA\Settings\SetupChecks\BruteForceThrottler; use OCA\Settings\SetupChecks\CheckUserCertificates; use OCA\Settings\SetupChecks\CodeIntegrity; +use OCA\Settings\SetupChecks\ConfigValidation; use OCA\Settings\SetupChecks\CronErrors; use OCA\Settings\SetupChecks\CronInfo; use OCA\Settings\SetupChecks\DatabaseHasMissingColumns; @@ -182,6 +183,7 @@ public function register(IRegistrationContext $context): void { $context->registerSetupCheck(BruteForceThrottler::class); $context->registerSetupCheck(CheckUserCertificates::class); $context->registerSetupCheck(CodeIntegrity::class); + $context->registerSetupCheck(ConfigValidation::class); $context->registerSetupCheck(CronErrors::class); $context->registerSetupCheck(CronInfo::class); $context->registerSetupCheck(DatabaseHasMissingColumns::class); diff --git a/apps/settings/lib/SetupChecks/ConfigValidation.php b/apps/settings/lib/SetupChecks/ConfigValidation.php new file mode 100644 index 0000000000000..e0cf091e68506 --- /dev/null +++ b/apps/settings/lib/SetupChecks/ConfigValidation.php @@ -0,0 +1,230 @@ +l10n->t('Config validation'); + } + + public function run(): SetupResult { + $proxyuserpwd = $this->config->getSystemValue('proxyuserpwd', self::FAKE_DEFAULT_VALUE); + if ($proxyuserpwd !== self::FAKE_DEFAULT_VALUE) { + if (!is_string($proxyuserpwd)) { + return SetupResult::error($this->l10n->t('Config value %s is not a string value.', ['proxyuserpwd'])); + } + + $parts = array_filter(explode(':', $proxyuserpwd)); + if (count($parts) !== 2) { + return SetupResult::error($this->l10n->t('Config value %s is not well formatted, expected format: non-empty-string, colon, non-empty-string (sample: "user:password").', ['proxyuserpwd'])); + } + } + + $overwritehost = $this->config->getSystemValue('overwritehost', self::FAKE_DEFAULT_VALUE); + if ($overwritehost !== self::FAKE_DEFAULT_VALUE) { + $return = $this->checkDomainOnly('overwritehost', $overwritehost); + if ($return) { + return $return; + } + } + + $trustedDomains = $this->config->getSystemValue('trusted_domains', self::FAKE_DEFAULT_VALUE); + if ($trustedDomains !== self::FAKE_DEFAULT_VALUE) { + if (!is_array($trustedDomains)) { + return SetupResult::error($this->l10n->t('Config value %s must be a list of strings with at least 1 entry.', ['trusted_domains'])); + } + + foreach ($trustedDomains as $key => $trustedDomain) { + if (!is_int($key)) { + return SetupResult::error($this->l10n->t('Config value %s must be a list of strings, but found a non-numeric key.', ['trusted_domains'])); + } + + // Resolve wildcards + $trustedDomain = str_replace('*', '1', $trustedDomain); + + $return = $this->checkDomainWithPort('trusted_domains => ' . $key, $trustedDomain); + if ($return) { + return $return; + } + } + } else { + return SetupResult::error($this->l10n->t('Config value %s must be a list of strings with at least 1 entry.', ['trusted_domains'])); + } + + $trustedProxies = $this->config->getSystemValue('trusted_proxies', self::FAKE_DEFAULT_VALUE); + if ($trustedProxies !== self::FAKE_DEFAULT_VALUE) { + if (!is_array($trustedProxies)) { + return SetupResult::error($this->l10n->t('Config value %s must be a list of strings.', ['trusted_proxies'])); + } + + foreach ($trustedProxies as $key => $trustedProxy) { + if (!is_int($key)) { + return SetupResult::error($this->l10n->t('Config value %s must be a list of strings, but found a non-numeric key.', ['trusted_proxies'])); + } + + $return = $this->checkIPOrIPRange('trusted_proxies => ' . $key, $trustedProxy); + if ($return) { + return $return; + } + } + } + + $fullURLConfigs = [ + 'overwrite.cli.url', + 'lost_password_link', + 'updater.server.url', + 'logo_url', + 'appstoreurl', + 'upgrade.cli-upgrade-link', + 'preview_imaginary_url', + 'lookup_server', + 'customclient_desktop', + 'customclient_android', + 'customclient_ios', + ]; + foreach ($fullURLConfigs as $configKey) { + $url = $this->config->getSystemValue($configKey, self::FAKE_DEFAULT_VALUE); + if ($url === self::FAKE_DEFAULT_VALUE) { + continue; + } + + $parsed = parse_url($url); + if ($parsed === false) { + return SetupResult::error($this->l10n->t('Config value %s is not a valid URL.', [$configKey])); + } + + $scheme = $parsed['scheme'] ?? ''; + if ($scheme !== 'https' && $scheme !== 'http') { + return SetupResult::error($this->l10n->t('Config value %s should be a full URL but has no protocol.', [$configKey])); + } + + if (isset($parsed['user']) || isset($parsed['pass'])) { + return SetupResult::error($this->l10n->t('Config value %s should not contain user nor password.', [$configKey])); + } + } + + $booleanConfigs = [ + 'auth.bruteforce.protection.enabled' => true, + 'auth.bruteforce.protection.testing' => false, + 'ratelimit.protection.enabled' => true, + ]; + + foreach ($booleanConfigs as $configKey => $expectedValue) { + $result = $this->checkBoolean($configKey, $expectedValue); + if ($result) { + return $result; + } + } + + return SetupResult::success($this->l10n->t('All configuration values look OK')); + } + + protected function checkBoolean(string $configKey, bool $shouldBe): ?SetupResult { + $value = $this->config->getSystemValue($configKey, self::FAKE_DEFAULT_VALUE); + if ($value === self::FAKE_DEFAULT_VALUE) { + return null; + } + + if (!is_bool($value)) { + return SetupResult::error($this->l10n->t('Config value %s is not a boolean value.', [$configKey])); + } + + if ($value !== $shouldBe) { + return SetupResult::warning($this->l10n->t('Config value %s should be %s in production instances.', [$configKey, json_encode($shouldBe)])); + } + return null; + } + + protected function checkDomainOnly(string $configKey, mixed $configValue): ?SetupResult { + if (!is_string($configValue)) { + return SetupResult::error($this->l10n->t('Config value %s is not a string value.', [$configKey])); + } + + if (str_starts_with($configValue, 'http:') || str_starts_with($configValue, 'https:')) { + return SetupResult::error($this->l10n->t('Config value %s should not contain a protocol.', [$configKey])); + } + + $url = 'https://' . $configValue; + $parts = parse_url($url); + if (!isset($parts['scheme'], $parts['host']) || count($parts) !== 2) { + return SetupResult::error($this->l10n->t('Config value %s should only contain a domain.', [$configKey])); + } + + return null; + } + + protected function checkDomainWithPort(string $configKey, mixed $configValue): ?SetupResult { + if (!is_string($configValue)) { + return SetupResult::error($this->l10n->t('Config value %s is not a string value.', [$configKey])); + } + + if (str_starts_with($configValue, 'http:') || str_starts_with($configValue, 'https:')) { + return SetupResult::error($this->l10n->t('Config value %s should not contain a protocol.', [$configKey])); + } + + $url = 'https://' . $configValue; + $parts = parse_url($url); + if ($parts === false || !isset($parts['scheme'], $parts['host']) || (count($parts) !== 2 && count($parts) !== 3)) { + return SetupResult::error($this->l10n->t('Config value %s should contain a domain or a domain and port.', [$configKey])); + } + if (!isset($parts['port']) && count($parts) === 3) { + return SetupResult::error($this->l10n->t('Config value %s should only contain a domain or domain and port.', [$configKey])); + } + + return null; + } + + protected function checkIPOrIPRange(string $configKey, mixed $configValue): ?SetupResult { + if (!is_string($configValue)) { + return SetupResult::error($this->l10n->t('Config value %s is not a string value.', [$configKey])); + } + + $parts = explode('/', $configValue); + if (count($parts) > 2) { + return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey])); + } + + if (count($parts) === 1) { + if (filter_var($parts[0], FILTER_VALIDATE_IP) === false) { + return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey])); + } + return null; + } + + if (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + if (!is_numeric($parts[1]) || $parts[1] > 32 || $parts[1] < 1) { + return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey])); + } + } elseif (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + if (!is_numeric($parts[1]) || $parts[1] > 128 || $parts[1] < 1) { + return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey])); + } + } else { + return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey])); + } + return null; + } +}