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

Allow to configure "allowed domains" for CORS on DAV #40537

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

susnux
Copy link
Contributor

@susnux susnux commented Sep 20, 2023

Summary

This adds CORS support to the DAV routes, the default is non breaking -> so no other domain is allowed.
But the admin can configure a list of allowed domains which will be allowed to access the DAV routes using CORS.

Moreover the admin can enable user defined lists, meaning that users can add their own allowed domains for WebDAV related requests, see #30964.

Why do we need CORS on DAV?

See #3131

Basically to access it within the browser from a different page, like e.g. the Dropbox file picker with allows you to pick files from your cloud onto another page (e.g. upload something from cloud to form etc).

Screenshot

Screenshot_20230920_150622

Todo

  • Follow-up: User config UI
  • Follow-up: allowed header UI
  • If this is decided: Write documentation

Checklist

* Exclude DAV CORS handling when no Origin specified
  This will exclude non-browser clients from CORS handling.
  Fixes some clients like davfs which break when CORS is enabled.
* fix: CORS on WebDAV is not working
  WebDAV is not working at all when used by on browser Javascript because the CORS headers
  are only present in the OPTION request, but not in the subsequent WebDAV methods.
  * This behavior is caused by a erroneous json_decode call while retriving the user's domains whitelist.
    It return an object, so the is_array always fails and no header are sent.
* Add Access-Control-Expose-Headers - to allow clients to access certain headers
* Adding many headers as allowed headers + add capability to read additional allowed headers from config.php
@susnux susnux added this to the Nextcloud 28 milestone Sep 20, 2023
@susnux susnux requested review from nickvergessen, skjnldsv, a team, ArtificialOwl and icewind1991 and removed request for a team September 20, 2023 13:15
I removed the beforeController logic here due to the change of handling CORS since PR 28457[1]

According to previous implementation, CORS was only allowed with methods that had @publicpage notation for preventing CSRF attacks.
But in the latest PR by me, the current implementations is as follows:

    * maintain a white-list of domains for whom CORS is enabled
    * This list can be viewed and edited under settings -> personal -> security

This implementation removes the need for `@PublicPage`[2].

[1] owncloud/core#28457
[2] owncloud/core#28864
@susnux susnux added 3. to review Waiting for reviews and removed 2. developing Work in progress labels Sep 20, 2023
github-advanced-security[bot]

This comment was marked as outdated.

@susnux susnux changed the title Allow to configure trusted domains for CORS on DAV Allow to configure "allowed domains" for CORS on DAV Sep 20, 2023
Also make sure to only return allowed methods for DAV responses

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
apps/dav/lib/Connector/Sabre/CorsPlugin.php Dismissed Show resolved Hide resolved
return;
}
} catch (\InvalidArgumentException $e) {
\OC::$server->getLogger()->debug('Invalid origin header was passed', ['Origin' => $originHeader, 'exception' => $e]);

Check notice

Code scanning / Psalm

DeprecatedMethod Note

The method OC\Server::getLogger has been marked as deprecated
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
\OC::$server->getLogger()->debug('Invalid origin header was passed', ['Origin' => $originHeader, 'exception' => $e]);
\OC::$server->get(LoggerInterface::class)->debug('Invalid origin header was passed', ['origin' => $originHeader, 'exception' => $e]);

apps/dav/lib/Connector/Sabre/CorsPlugin.php Dismissed Show dismissed Hide dismissed
apps/dav/lib/Connector/Sabre/CorsPlugin.php Dismissed Show dismissed Hide dismissed
@@ -130,6 +131,8 @@
$this->server->addPlugin(new ProfilerPlugin($this->request));
$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
$this->server->addPlugin(new AnonymousOptionsPlugin());
$this->server->addPlugin(new CorsPlugin(\OC::$server->getUserSession(), \OC::$server->getConfig()));

Check notice

Code scanning / Psalm

DeprecatedMethod Note

The method OC\Server::getUserSession has been marked as deprecated
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can change to:

Suggested change
$this->server->addPlugin(new CorsPlugin(\OC::$server->getUserSession(), \OC::$server->getConfig()));
$this->server->addPlugin(new CorsPlugin(\OC::$server->get(IUserSession), \OC::$server->get(IConfig::class)));

With:

use \OCP\IConfig;
use \OCP\IUserSession

apps/dav/lib/Server.php Dismissed Show dismissed Hide dismissed
* @return DataResponse
*/
public function updateUserEnabled($value) {
if (!is_bool($value)) {

Check notice

Code scanning / Psalm

DocblockTypeContradiction Note

Docblock-defined type bool for $value is always bool

// Reach here if it's valid
$response = new \OC\OCS\Result(null, 100, 'OPTIONS request successful');
\OC_Response::setOptionsRequestHeaders($response, $this->config);

Check failure

Code scanning / Psalm

InvalidArgument Error

Argument 1 of OC_Response::setOptionsRequestHeaders expects OCP\AppFramework\Http\Response<int, array<string, mixed>>|Sabre\HTTP\ResponseInterface, but OC\OCS\Result provided
@nickvergessen nickvergessen removed their request for review September 21, 2023 08:37
@skjnldsv skjnldsv removed their request for review September 22, 2023 09:16
@skjnldsv

This comment was marked as resolved.

Copy link
Collaborator

@Altahrim Altahrim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First quick read ^^

apps/dav/lib/Connector/Sabre/CorsPlugin.php Dismissed Show resolved Hide resolved
Comment on lines +68 to +70
if (!$request->hasHeader('Origin')) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can skip this part. The test $originHeader === null on line 72 is equivalent

return;
}
} catch (\InvalidArgumentException $e) {
\OC::$server->getLogger()->debug('Invalid origin header was passed', ['Origin' => $originHeader, 'exception' => $e]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
\OC::$server->getLogger()->debug('Invalid origin header was passed', ['Origin' => $originHeader, 'exception' => $e]);
\OC::$server->get(LoggerInterface::class)->debug('Invalid origin header was passed', ['origin' => $originHeader, 'exception' => $e]);

*/
public function setOptionsRequestHeaders(RequestInterface $request, ResponseInterface $response) {
$authorization = $request->getHeader('Authorization');
if ($authorization === null || $authorization === '') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

Suggested change
if ($authorization === null || $authorization === '') {
if (empty($authorization)) {

@@ -130,6 +131,8 @@
$this->server->addPlugin(new ProfilerPlugin($this->request));
$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
$this->server->addPlugin(new AnonymousOptionsPlugin());
$this->server->addPlugin(new CorsPlugin(\OC::$server->getUserSession(), \OC::$server->getConfig()));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can change to:

Suggested change
$this->server->addPlugin(new CorsPlugin(\OC::$server->getUserSession(), \OC::$server->getConfig()));
$this->server->addPlugin(new CorsPlugin(\OC::$server->get(IUserSession), \OC::$server->get(IConfig::class)));

With:

use \OCP\IConfig;
use \OCP\IUserSession

Comment on lines +654 to +660
if ($protocol === 'http') {
$port = 80;
} elseif ($protocol === 'https') {
$port = 443;
} else {
throw new \InvalidArgumentException('Only http based URLs supported');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorter version:

Suggested change
if ($protocol === 'http') {
$port = 80;
} elseif ($protocol === 'https') {
$port = 443;
} else {
throw new \InvalidArgumentException('Only http based URLs supported');
}
$port = match($protocol) {
'http' => 80,
'https' => 443,
};

Comment on lines +58 to +70
public function __construct($AppName, IRequest $request,
IUserSession $userSession,
ILogger $logger,
IURLGenerator $urlGenerator,
IConfig $config) {
parent::__construct($AppName, $request);

$this->AppName = $AppName;
$this->config = $config;
$this->userId = $userSession->getUser()->getUID();
$this->logger = $logger;
$this->urlGenerator = $urlGenerator;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use properties promotion:

Suggested change
public function __construct($AppName, IRequest $request,
IUserSession $userSession,
ILogger $logger,
IURLGenerator $urlGenerator,
IConfig $config) {
parent::__construct($AppName, $request);
$this->AppName = $AppName;
$this->config = $config;
$this->userId = $userSession->getUser()->getUID();
$this->logger = $logger;
$this->urlGenerator = $urlGenerator;
}
public function __construct(
$AppName, IRequest $request,
IUserSession $userSession,
private LoggerInterface $logger,
private IURLGenerator $urlGenerator,
private IConfig $config
) {
parent::__construct($AppName, $request);
$this->userId = $userSession->getUser()->getUID();
}

Also replace deprecated ILogger

Comment on lines +93 to +97
if (empty($this->config->getUserValue($userId, 'core', 'domains'))) {
$domains = [];
} else {
$domains = json_decode($this->config->getUserValue($userId, 'core', 'domains'));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (empty($this->config->getUserValue($userId, 'core', 'domains'))) {
$domains = [];
} else {
$domains = json_decode($this->config->getUserValue($userId, 'core', 'domains'));
}
$domains = empty($this->config->getUserValue($userId, 'core', 'domains')
? []
: json_decode($this->config->getUserValue($userId, 'core', 'domains'), flags: JSON_THROW_ON_ERROR);

* @param string $domain The domain to whitelist
* @return RedirectResponse Redirection to the settings page.
*/
public function addDomain($domain) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we enforce it here?

Suggested change
public function addDomain($domain) {
public function addDomain(string $domain) {

* @return RedirectResponse Redirection to the settings page.
*/
public function addDomain($domain) {
if (!isset($domain) || !self::isValidUrl($domain)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so:

Suggested change
if (!isset($domain) || !self::isValidUrl($domain)) {
if ($domain === '' || !self::isValidUrl($domain)) {

@skjnldsv skjnldsv mentioned this pull request Nov 1, 2023
This was referenced Nov 6, 2023
This was referenced Nov 14, 2023
@blizzz blizzz modified the milestones: Nextcloud 28, Nextcloud 29 Nov 23, 2023
This was referenced Mar 12, 2024
This was referenced Mar 20, 2024
@skjnldsv skjnldsv mentioned this pull request Mar 28, 2024
81 tasks
@skjnldsv skjnldsv modified the milestones: Nextcloud 29, Nextcloud 30 Mar 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cors origin allowed list check CORS setting at user level Support for cross-domain WebDAV access (CORS)
5 participants