Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>pull/6630/head
parent
4ee731c1ab
commit
c257cd57d4
@ -0,0 +1,38 @@ |
||||
<?php |
||||
/** |
||||
* @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
|
||||
namespace OC\AppFramework\Middleware\Security\Exceptions; |
||||
|
||||
use OCP\AppFramework\Http; |
||||
|
||||
/** |
||||
* Class LaxSameSiteCookieFailedException is thrown when a request doesn't pass |
||||
* the required LaxCookie check on index.php |
||||
* |
||||
* @package OC\AppFramework\Middleware\Security\Exceptions |
||||
*/ |
||||
class LaxSameSiteCookieFailedException extends SecurityException { |
||||
public function __construct() { |
||||
parent::__construct('Lax Same Site Cookie is invalid in request.', Http::STATUS_PRECONDITION_FAILED); |
||||
} |
||||
} |
||||
@ -0,0 +1,106 @@ |
||||
<?php |
||||
/** |
||||
* @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
namespace OC\AppFramework\Middleware\Security; |
||||
|
||||
use OC\AppFramework\Http\Request; |
||||
use OC\AppFramework\Middleware\Security\Exceptions\LaxSameSiteCookieFailedException; |
||||
use OC\AppFramework\Utility\ControllerMethodReflector; |
||||
use OCP\AppFramework\Http; |
||||
use OCP\AppFramework\Http\Response; |
||||
use OCP\AppFramework\Middleware; |
||||
|
||||
class SameSiteCookieMiddleware extends Middleware { |
||||
|
||||
/** @var Request */ |
||||
private $request; |
||||
|
||||
/** @var ControllerMethodReflector */ |
||||
private $reflector; |
||||
|
||||
public function __construct(Request $request, |
||||
ControllerMethodReflector $reflector) { |
||||
$this->request = $request; |
||||
$this->reflector = $reflector; |
||||
} |
||||
|
||||
public function beforeController($controller, $methodName) { |
||||
$requestUri = $this->request->getScriptName(); |
||||
$processingScript = explode('/', $requestUri); |
||||
$processingScript = $processingScript[count($processingScript)-1]; |
||||
|
||||
if ($processingScript !== 'index.php') { |
||||
return; |
||||
} |
||||
|
||||
$noSSC = $this->reflector->hasAnnotation('NoSameSiteCookieRequired'); |
||||
if ($noSSC) { |
||||
return; |
||||
} |
||||
|
||||
if (!$this->request->passesLaxCookieCheck()) { |
||||
throw new LaxSameSiteCookieFailedException(); |
||||
} |
||||
} |
||||
|
||||
public function afterException($controller, $methodName, \Exception $exception) { |
||||
if ($exception instanceof LaxSameSiteCookieFailedException) { |
||||
$respone = new Response(); |
||||
$respone->setStatus(Http::STATUS_FOUND); |
||||
$respone->addHeader('Location', $this->request->getRequestUri()); |
||||
|
||||
$this->setSameSiteCookie(); |
||||
|
||||
return $respone; |
||||
} |
||||
|
||||
throw $exception; |
||||
} |
||||
|
||||
protected function setSameSiteCookie() { |
||||
$cookieParams = $this->request->getCookieParams(); |
||||
$secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : ''; |
||||
$policies = [ |
||||
'lax', |
||||
'strict', |
||||
]; |
||||
|
||||
// Append __Host to the cookie if it meets the requirements |
||||
$cookiePrefix = ''; |
||||
if($cookieParams['secure'] === true && $cookieParams['path'] === '/') { |
||||
$cookiePrefix = '__Host-'; |
||||
} |
||||
|
||||
foreach($policies as $policy) { |
||||
header( |
||||
sprintf( |
||||
'Set-Cookie: %snc_sameSiteCookie%s=true; path=%s; httponly;' . $secureCookie . 'expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=%s', |
||||
$cookiePrefix, |
||||
$policy, |
||||
$cookieParams['path'], |
||||
$policy |
||||
), |
||||
false |
||||
); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,133 @@ |
||||
<?php |
||||
/** |
||||
* @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
namespace Test\AppFramework\Middleware\Security; |
||||
|
||||
use OC\AppFramework\Http\Request; |
||||
use OC\AppFramework\Middleware\Security\Exceptions\LaxSameSiteCookieFailedException; |
||||
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; |
||||
use OC\AppFramework\Middleware\Security\SameSiteCookieMiddleware; |
||||
use OC\AppFramework\Utility\ControllerMethodReflector; |
||||
use OCP\AppFramework\Controller; |
||||
use OCP\AppFramework\Http; |
||||
use Test\TestCase; |
||||
|
||||
class SameSiteCookieMiddlewareTest extends TestCase { |
||||
|
||||
/** @var SameSiteCookieMiddleware */ |
||||
private $middleware; |
||||
|
||||
/** @var Request|\PHPUnit_Framework_MockObject_MockObject */ |
||||
private $request; |
||||
|
||||
/** @var ControllerMethodReflector|\PHPUnit_Framework_MockObject_MockObject */ |
||||
private $reflector; |
||||
|
||||
public function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$this->request = $this->createMock(Request::class); |
||||
$this->reflector = $this->createMock(ControllerMethodReflector::class); |
||||
$this->middleware = new SameSiteCookieMiddleware($this->request, $this->reflector); |
||||
} |
||||
|
||||
public function testBeforeControllerNoIndex() { |
||||
$this->request->method('getScriptName') |
||||
->willReturn('/ocs/v2.php'); |
||||
|
||||
$this->middleware->beforeController($this->createMock(Controller::class), 'foo'); |
||||
} |
||||
|
||||
public function testBeforeControllerIndexHasAnnotation() { |
||||
$this->request->method('getScriptName') |
||||
->willReturn('/index.php'); |
||||
|
||||
$this->reflector->method('hasAnnotation') |
||||
->with('NoSameSiteCookieRequired') |
||||
->willReturn(true); |
||||
|
||||
$this->middleware->beforeController($this->createMock(Controller::class), 'foo'); |
||||
} |
||||
|
||||
public function testBeforeControllerIndexNoAnnotationPassingCheck() { |
||||
$this->request->method('getScriptName') |
||||
->willReturn('/index.php'); |
||||
|
||||
$this->reflector->method('hasAnnotation') |
||||
->with('NoSameSiteCookieRequired') |
||||
->willReturn(false); |
||||
|
||||
$this->request->method('passesLaxCookieCheck') |
||||
->willReturn(true); |
||||
|
||||
$this->middleware->beforeController($this->createMock(Controller::class), 'foo'); |
||||
} |
||||
|
||||
public function testBeforeControllerIndexNoAnnotationFailingCheck() { |
||||
$this->expectException(LaxSameSiteCookieFailedException::class); |
||||
|
||||
$this->request->method('getScriptName') |
||||
->willReturn('/index.php'); |
||||
|
||||
$this->reflector->method('hasAnnotation') |
||||
->with('NoSameSiteCookieRequired') |
||||
->willReturn(false); |
||||
|
||||
$this->request->method('passesLaxCookieCheck') |
||||
->willReturn(false); |
||||
|
||||
$this->middleware->beforeController($this->createMock(Controller::class), 'foo'); |
||||
} |
||||
|
||||
public function testAfterExceptionNoLaxCookie() { |
||||
$ex = new SecurityException(); |
||||
|
||||
try { |
||||
$this->middleware->afterException($this->createMock(Controller::class), 'foo', $ex); |
||||
$this->fail(); |
||||
} catch (\Exception $e) { |
||||
$this->assertSame($ex, $e); |
||||
} |
||||
} |
||||
|
||||
public function testAfterExceptionLaxCookie() { |
||||
$ex = new LaxSameSiteCookieFailedException(); |
||||
|
||||
$this->request->method('getRequestUri') |
||||
->willReturn('/myrequri'); |
||||
|
||||
$middleware = $this->getMockBuilder(SameSiteCookieMiddleware::class) |
||||
->setConstructorArgs([$this->request, $this->reflector]) |
||||
->setMethods(['setSameSiteCookie']) |
||||
->getMock(); |
||||
|
||||
$middleware->expects($this->once()) |
||||
->method('setSameSiteCookie'); |
||||
|
||||
$resp = $middleware->afterException($this->createMock(Controller::class), 'foo', $ex); |
||||
|
||||
$this->assertSame(Http::STATUS_FOUND, $resp->getStatus()); |
||||
|
||||
$headers = $resp->getHeaders(); |
||||
$this->assertSame('/myrequri', $headers['Location']); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue