Skip to content

Commit bd8d50d

Browse files
authored
Merge pull request #44 from nextcloud/stable9-add-same-site-cookies
[stable9] Add Same Site Cookie protection
2 parents 7a10bed + 2c6a5fc commit bd8d50d

File tree

11 files changed

+520
-30
lines changed

11 files changed

+520
-30
lines changed

apps/files/controller/apicontroller.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public function __construct($appName,
8080
*
8181
* @NoAdminRequired
8282
* @NoCSRFRequired
83+
* @StrictCookieRequired
8384
*
8485
* @param int $x
8586
* @param int $y

lib/base.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,70 @@ public static function setRequiredIniValues() {
480480
@ini_set('gd.jpeg_ignore_warning', 1);
481481
}
482482

483+
/**
484+
* Send the same site cookies
485+
*/
486+
private static function sendSameSiteCookies() {
487+
$cookieParams = session_get_cookie_params();
488+
$secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : '';
489+
$policies = [
490+
'lax',
491+
'strict',
492+
];
493+
foreach($policies as $policy) {
494+
header(
495+
sprintf(
496+
'Set-Cookie: nc_sameSiteCookie%s=true; path=%s; httponly;' . $secureCookie . 'expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=%s',
497+
$policy,
498+
$cookieParams['path'],
499+
$policy
500+
),
501+
false
502+
);
503+
}
504+
}
505+
506+
/**
507+
* Same Site cookie to further mitigate CSRF attacks. This cookie has to
508+
* be set in every request if cookies are sent to add a second level of
509+
* defense against CSRF.
510+
*
511+
* If the cookie is not sent this will set the cookie and reload the page.
512+
* We use an additional cookie since we want to protect logout CSRF and
513+
* also we can't directly interfere with PHP's session mechanism.
514+
*/
515+
private static function performSameSiteCookieProtection() {
516+
if(count($_COOKIE) > 0) {
517+
$request = \OC::$server->getRequest();
518+
$requestUri = $request->getScriptName();
519+
$processingScript = explode('/', $requestUri);
520+
$processingScript = $processingScript[count($processingScript)-1];
521+
522+
// For the "index.php" endpoint only a lax cookie is required.
523+
if($processingScript === 'index.php') {
524+
if(!$request->passesLaxCookieCheck()) {
525+
self::sendSameSiteCookies();
526+
header('Location: '.$_SERVER['REQUEST_URI']);
527+
exit();
528+
}
529+
} else {
530+
// All other endpoints require the lax and the strict cookie
531+
if(!$request->passesStrictCookieCheck()) {
532+
self::sendSameSiteCookies();
533+
// Debug mode gets access to the resources without strict cookie
534+
// due to the fact that the SabreDAV browser also lives there.
535+
if(!\OC::$server->getConfig()->getSystemValue('debug', false)) {
536+
http_response_code(\OCP\AppFramework\Http::STATUS_SERVICE_UNAVAILABLE);
537+
exit();
538+
}
539+
}
540+
}
541+
} elseif(!isset($_COOKIE['nc_sameSiteCookielax']) || !isset($_COOKIE['nc_sameSiteCookiestrict'])) {
542+
self::sendSameSiteCookies();
543+
}
544+
}
545+
546+
483547
public static function init() {
484548
// calculate the root directories
485549
OC::$SERVERROOT = str_replace("\\", '/', substr(__DIR__, 0, -4));
@@ -589,6 +653,8 @@ public static function init() {
589653
ini_set('session.cookie_secure', true);
590654
}
591655

656+
self::performSameSiteCookieProtection();
657+
592658
if (!defined('OC_CONSOLE')) {
593659
$errors = OC_Util::checkServer(\OC::$server->getConfig());
594660
if (count($errors) > 0) {

lib/private/appframework/http/request.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,10 @@ public function passesCSRFCheck() {
448448
return false;
449449
}
450450

451+
if(!$this->passesStrictCookieCheck()) {
452+
return false;
453+
}
454+
451455
if (isset($this->items['get']['requesttoken'])) {
452456
$token = $this->items['get']['requesttoken'];
453457
} elseif (isset($this->items['post']['requesttoken'])) {
@@ -463,6 +467,35 @@ public function passesCSRFCheck() {
463467
return $this->csrfTokenManager->isTokenValid($token);
464468
}
465469

470+
/**
471+
* Checks if the strict cookie has been sent with the request
472+
*
473+
* @return bool
474+
* @since 9.1.0
475+
*/
476+
public function passesStrictCookieCheck() {
477+
if($this->getCookie('nc_sameSiteCookiestrict') === 'true'
478+
&& $this->passesLaxCookieCheck()) {
479+
return true;
480+
}
481+
482+
return false;
483+
}
484+
485+
/**
486+
* Checks if the lax cookie has been sent with the request
487+
*
488+
* @return bool
489+
* @since 9.1.0
490+
*/
491+
public function passesLaxCookieCheck() {
492+
if($this->getCookie('nc_sameSiteCookielax') === 'true') {
493+
return true;
494+
}
495+
496+
return false;
497+
}
498+
466499
/**
467500
* Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
468501
* If `mod_unique_id` is installed this value will be taken.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
/**
3+
* @author Lukas Reschke <lukas@statuscode.ch>
4+
*
5+
* @license GNU AGPL version 3 or any later version
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as
9+
* published by the Free Software Foundation, either version 3 of the
10+
* License, or (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
*/
21+
22+
namespace OC\Appframework\Middleware\Security\Exceptions;
23+
24+
use OCP\AppFramework\Http;
25+
26+
/**
27+
* Class StrictCookieMissingException is thrown when the strict cookie has not
28+
* been sent with the request but is required.
29+
*
30+
* @package OC\Appframework\Middleware\Security\Exceptions
31+
*/
32+
class StrictCookieMissingException extends SecurityException {
33+
public function __construct() {
34+
parent::__construct('Strict Cookie has not been found in request.', Http::STATUS_PRECONDITION_FAILED);
35+
}
36+
}

lib/private/appframework/middleware/security/securitymiddleware.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use OC\Appframework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
3131
use OC\Appframework\Middleware\Security\Exceptions\NotAdminException;
3232
use OC\Appframework\Middleware\Security\Exceptions\NotLoggedInException;
33+
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
3334
use OC\AppFramework\Utility\ControllerMethodReflector;
3435
use OC\Security\CSP\ContentSecurityPolicyManager;
3536
use OCP\AppFramework\Http\ContentSecurityPolicy;
@@ -132,6 +133,13 @@ public function beforeController($controller, $methodName) {
132133
}
133134
}
134135

136+
// Check for strict cookie requirement
137+
if($this->reflector->hasAnnotation('StrictCookieRequired') || !$this->reflector->hasAnnotation('NoCSRFRequired')) {
138+
if(!$this->request->passesStrictCookieCheck()) {
139+
throw new StrictCookieMissingException();
140+
}
141+
}
142+
135143
// CSRF check - also registers the CSRF token since the session may be closed later
136144
Util::callRegister();
137145
if(!$this->reflector->hasAnnotation('NoCSRFRequired')) {
@@ -184,6 +192,9 @@ public function afterController($controller, $methodName, Response $response) {
184192
*/
185193
public function afterException($controller, $methodName, \Exception $exception) {
186194
if($exception instanceof SecurityException) {
195+
if($exception instanceof StrictCookieMissingException) {
196+
return new RedirectResponse(\OC::$WEBROOT);
197+
}
187198

188199
if (stripos($this->request->getHeader('Accept'),'html') === false) {
189200
$response = new JSONResponse(

lib/private/eventsource.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ protected function init() {
7676
} else {
7777
header("Content-Type: text/event-stream");
7878
}
79+
if(!\OC::$server->getRequest()->passesStrictCookieCheck()) {
80+
header('Location: '.\OC::$WEBROOT);
81+
exit();
82+
}
7983
if (!(\OC::$server->getRequest()->passesCSRFCheck())) {
8084
$this->send('error', 'Possible CSRF attack. Connection will be closed.');
8185
$this->close();

lib/private/json.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ public static function checkLoggedIn() {
7777
* @deprecated Use annotation based CSRF checks from the AppFramework instead
7878
*/
7979
public static function callCheck() {
80+
if(!\OC::$server->getRequest()->passesStrictCookieCheck()) {
81+
header('Location: '.\OC::$WEBROOT);
82+
exit();
83+
}
84+
8085
if( !(\OC::$server->getRequest()->passesCSRFCheck())) {
8186
$l = \OC::$server->getL10N('lib');
8287
self::error(array( 'data' => array( 'message' => $l->t('Token expired. Please reload page.'), 'error' => 'token_expired' )));

lib/public/irequest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ public function getCookie($key);
143143
*/
144144
public function passesCSRFCheck();
145145

146+
/**
147+
* Checks if the strict cookie has been sent with the request
148+
*
149+
* @return bool
150+
* @since 9.0.0
151+
*/
152+
public function passesStrictCookieCheck();
153+
154+
/**
155+
* Checks if the lax cookie has been sent with the request
156+
*
157+
* @return bool
158+
* @since 9.1.0
159+
*/
160+
public function passesLaxCookieCheck();
161+
146162
/**
147163
* Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
148164
* If `mod_unique_id` is installed this value will be taken.

lib/public/util.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,11 @@ public static function callRegister() {
504504
* @deprecated 9.0.0 Use annotations based on the app framework.
505505
*/
506506
public static function callCheck() {
507+
if(!\OC::$server->getRequest()->passesStrictCookieCheck()) {
508+
header('Location: '.\OC::$WEBROOT);
509+
exit();
510+
}
511+
507512
if (!(\OC::$server->getRequest()->passesCSRFCheck())) {
508513
exit();
509514
}

0 commit comments

Comments
 (0)