Compare commits

...

1 Commits

Author SHA1 Message Date
Daniel Kesselberg 16543e07b4 refactor: move csrf validation out of request
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2025-10-24 01:06:25 +02:00
24 changed files with 380 additions and 66 deletions
+2
View File
@@ -36,6 +36,7 @@ use OCP\L10N\IFactory as IL10NFactory;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ISecureRandom;
use OCP\Server;
use OCP\Security\CSRF\ICsrfValidator;
use Psr\Log\LoggerInterface;
$authBackend = new Auth(
@@ -44,6 +45,7 @@ $authBackend = new Auth(
Server::get(IRequest::class),
Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
Server::get(IThrottler::class),
Server::get(ICsrfValidator::class),
'principals/'
);
$principalBackend = new Principal(
+2
View File
@@ -32,6 +32,7 @@ use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Server;
use OCP\Security\CSRF\ICsrfValidator;
use Psr\Log\LoggerInterface;
use Sabre\CardDAV\Plugin;
@@ -41,6 +42,7 @@ $authBackend = new Auth(
Server::get(IRequest::class),
Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
Server::get(IThrottler::class),
Server::get(ICsrfValidator::class),
'principals/'
);
$principalBackend = new Principal(
+2
View File
@@ -22,6 +22,7 @@ use OCP\IUserSession;
use OCP\SabrePluginEvent;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Server;
use OCP\Security\CSRF\ICsrfValidator;
use Psr\Log\LoggerInterface;
// no php execution timeout for webdav
@@ -55,6 +56,7 @@ $authBackend = new Auth(
Server::get(IRequest::class),
Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
Server::get(IThrottler::class),
Server::get(ICsrfValidator::class),
'principals/'
);
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
+3 -1
View File
@@ -20,6 +20,7 @@ use OCP\ISession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
use OCP\Server;
use OCP\Security\CSRF\ICsrfValidator;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Exception\NotAuthenticated;
@@ -37,6 +38,7 @@ class Auth extends AbstractBasic {
private IRequest $request,
private Manager $twoFactorManager,
private IThrottler $throttler,
private ICsrfValidator $csrfValidator,
string $principalPrefix = 'principals/users/',
) {
$this->principalPrefix = $principalPrefix;
@@ -162,7 +164,7 @@ class Auth extends AbstractBasic {
private function auth(RequestInterface $request, ResponseInterface $response): array {
$forcedLogout = false;
if (!$this->request->passesCSRFCheck()
if (!$this->csrfValidator->validate($this->request)
&& $this->requiresCSRFCheck()) {
// In case of a fail with POST we need to recheck the credentials
if ($this->request->getMethod() === 'POST') {
+3 -1
View File
@@ -98,6 +98,7 @@ use OCP\SabrePluginEvent;
use OCP\Security\Bruteforce\IThrottler;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\Security\CSRF\ICsrfValidator;
use Psr\Log\LoggerInterface;
use Sabre\CardDAV\VCFExportPlugin;
use Sabre\DAV\Auth\Plugin;
@@ -138,7 +139,8 @@ class Server {
\OCP\Server::get(IUserSession::class),
\OCP\Server::get(IRequest::class),
\OCP\Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
\OCP\Server::get(IThrottler::class)
\OCP\Server::get(IThrottler::class),
\OCP\Server::get(ICsrfValidator::class)
);
// Set URL explicitly due to reverse-proxy situations
@@ -18,6 +18,7 @@ use OCP\ISession;
use OCP\IUser;
use OCP\Security\Bruteforce\IThrottler;
use PHPUnit\Framework\MockObject\MockObject;
use OCP\Security\CSRF\ICsrfValidator;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -35,6 +36,7 @@ class AuthTest extends TestCase {
private IRequest&MockObject $request;
private Manager&MockObject $twoFactorManager;
private IThrottler&MockObject $throttler;
private ICsrfValidator $csrfValidator;
private Auth $auth;
protected function setUp(): void {
@@ -44,12 +46,14 @@ class AuthTest extends TestCase {
$this->request = $this->createMock(IRequest::class);
$this->twoFactorManager = $this->createMock(Manager::class);
$this->throttler = $this->createMock(IThrottler::class);
$this->csrfValidator = $this->createMock(ICsrfValidator::class);
$this->auth = new Auth(
$this->session,
$this->userSession,
$this->request,
$this->twoFactorManager,
$this->throttler
$this->throttler,
$this->csrfValidator,
);
}
@@ -228,9 +232,9 @@ class AuthTest extends TestCase {
->expects($this->any())
->method('getUser')
->willReturn($user);
$this->request
$this->csrfValidator
->expects($this->once())
->method('passesCSRFCheck')
->method('validate')
->willReturn(false);
$expectedResponse = [
@@ -269,9 +273,9 @@ class AuthTest extends TestCase {
->expects($this->any())
->method('getUser')
->willReturn($user);
$this->request
$this->csrfValidator
->expects($this->once())
->method('passesCSRFCheck')
->method('validate')
->willReturn(false);
$this->auth->check($request, $response);
}
@@ -308,9 +312,9 @@ class AuthTest extends TestCase {
->expects($this->any())
->method('getUser')
->willReturn($user);
$this->request
$this->csrfValidator
->expects($this->once())
->method('passesCSRFCheck')
->method('validate')
->willReturn(true);
$this->twoFactorManager->expects($this->once())
->method('needsSecondFactor')
@@ -351,9 +355,9 @@ class AuthTest extends TestCase {
->expects($this->any())
->method('getUser')
->willReturn($user);
$this->request
$this->csrfValidator
->expects($this->once())
->method('passesCSRFCheck')
->method('validate')
->willReturn(false);
$this->auth->check($request, $response);
}
@@ -386,9 +390,9 @@ class AuthTest extends TestCase {
->expects($this->any())
->method('getUser')
->willReturn($user);
$this->request
$this->csrfValidator
->expects($this->once())
->method('passesCSRFCheck')
->method('validate')
->willReturn(false);
$this->auth->check($request, $response);
@@ -443,9 +447,9 @@ class AuthTest extends TestCase {
->expects($this->any())
->method('getUser')
->willReturn($user);
$this->request
$this->csrfValidator
->expects($this->once())
->method('passesCSRFCheck')
->method('validate')
->willReturn(true);
$response = $this->auth->check($request, $response);
+3 -1
View File
@@ -44,6 +44,7 @@ use OCP\Notification\IManager;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ITrustedDomainHelper;
use OCP\Server;
use OCP\Security\CSRF\ICsrfValidator;
use OCP\Util;
class LoginController extends Controller {
@@ -67,6 +68,7 @@ class LoginController extends Controller {
private IManager $manager,
private IL10N $l10n,
private IAppManager $appManager,
private ICsrfValidator $csrfValidator,
) {
parent::__construct($appName, $request);
}
@@ -301,7 +303,7 @@ class LoginController extends Controller {
// This could have come from someone malicious who tries to block a user by triggering the bruteforce protection.
$error = self::LOGIN_MSG_INVALID_ORIGIN;
$throttle = false;
} elseif (!$this->request->passesCSRFCheck()) {
} elseif (!$this->csrfValidator->validate($this->request)) {
if ($this->userSession->isLoggedIn()) {
// If the user is already logged in and the CSRF check does not pass then
// simply redirect the user to the correct page as required. This is the
@@ -757,6 +757,7 @@ return array(
'OCP\\Security\\Bruteforce\\IThrottler' => $baseDir . '/lib/public/Security/Bruteforce/IThrottler.php',
'OCP\\Security\\Bruteforce\\MaxDelayReached' => $baseDir . '/lib/public/Security/Bruteforce/MaxDelayReached.php',
'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => $baseDir . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php',
'OCP\\Security\\CSRF\\ICsrfValidator' => $baseDir . '/lib/public/Security/CSRF/ICsrfValidator.php',
'OCP\\Security\\Events\\GenerateSecurePasswordEvent' => $baseDir . '/lib/public/Security/Events/GenerateSecurePasswordEvent.php',
'OCP\\Security\\Events\\ValidatePasswordPolicyEvent' => $baseDir . '/lib/public/Security/Events/ValidatePasswordPolicyEvent.php',
'OCP\\Security\\FeaturePolicy\\AddFeaturePolicyEvent' => $baseDir . '/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php',
@@ -2032,6 +2033,7 @@ return array(
'OC\\Security\\CSRF\\CsrfToken' => $baseDir . '/lib/private/Security/CSRF/CsrfToken.php',
'OC\\Security\\CSRF\\CsrfTokenGenerator' => $baseDir . '/lib/private/Security/CSRF/CsrfTokenGenerator.php',
'OC\\Security\\CSRF\\CsrfTokenManager' => $baseDir . '/lib/private/Security/CSRF/CsrfTokenManager.php',
'OC\\Security\\CSRF\\CsrfValidator' => $baseDir . '/lib/private/Security/CSRF/CsrfValidator.php',
'OC\\Security\\CSRF\\TokenStorage\\SessionStorage' => $baseDir . '/lib/private/Security/CSRF/TokenStorage/SessionStorage.php',
'OC\\Security\\Certificate' => $baseDir . '/lib/private/Security/Certificate.php',
'OC\\Security\\CertificateManager' => $baseDir . '/lib/private/Security/CertificateManager.php',
@@ -798,6 +798,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Security\\Bruteforce\\IThrottler' => __DIR__ . '/../../..' . '/lib/public/Security/Bruteforce/IThrottler.php',
'OCP\\Security\\Bruteforce\\MaxDelayReached' => __DIR__ . '/../../..' . '/lib/public/Security/Bruteforce/MaxDelayReached.php',
'OCP\\Security\\CSP\\AddContentSecurityPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/CSP/AddContentSecurityPolicyEvent.php',
'OCP\\Security\\CSRF\\ICsrfValidator' => __DIR__ . '/../../..' . '/lib/public/Security/CSRF/ICsrfValidator.php',
'OCP\\Security\\Events\\GenerateSecurePasswordEvent' => __DIR__ . '/../../..' . '/lib/public/Security/Events/GenerateSecurePasswordEvent.php',
'OCP\\Security\\Events\\ValidatePasswordPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/Events/ValidatePasswordPolicyEvent.php',
'OCP\\Security\\FeaturePolicy\\AddFeaturePolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php',
@@ -2073,6 +2074,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\CSRF\\CsrfToken' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/CsrfToken.php',
'OC\\Security\\CSRF\\CsrfTokenGenerator' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/CsrfTokenGenerator.php',
'OC\\Security\\CSRF\\CsrfTokenManager' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/CsrfTokenManager.php',
'OC\\Security\\CSRF\\CsrfValidator' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/CsrfValidator.php',
'OC\\Security\\CSRF\\TokenStorage\\SessionStorage' => __DIR__ . '/../../..' . '/lib/private/Security/CSRF/TokenStorage/SessionStorage.php',
'OC\\Security\\Certificate' => __DIR__ . '/../../..' . '/lib/private/Security/Certificate.php',
'OC\\Security\\CertificateManager' => __DIR__ . '/../../..' . '/lib/private/Security/CertificateManager.php',
@@ -59,6 +59,8 @@ use OCP\IServerContainer;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\CSRF\ICsrfValidator;
use OCP\Security\Ip\IRemoteAddress;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
@@ -206,6 +208,7 @@ class DIContainer extends SimpleContainer implements IAppContainer {
$c->get(AuthorizedGroupMapper::class),
$c->get(IUserSession::class),
$c->get(IRemoteAddress::class),
$c->get(ICsrfValidator::class),
);
$dispatcher->registerMiddleware($securityMiddleware);
$dispatcher->registerMiddleware($c->get(CSPMiddleware::class));
@@ -21,6 +21,7 @@ use OCP\AppFramework\Middleware;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\CSRF\ICsrfValidator;
use Psr\Log\LoggerInterface;
use ReflectionMethod;
@@ -46,6 +47,7 @@ class CORSMiddleware extends Middleware {
Session $session,
IThrottler $throttler,
private readonly LoggerInterface $logger,
private readonly ICsrfValidator $csrfValidator,
) {
$this->request = $request;
$this->reflector = $reflector;
@@ -74,7 +76,7 @@ class CORSMiddleware extends Middleware {
$pass = array_key_exists('PHP_AUTH_PW', $this->request->server) ? $this->request->server['PHP_AUTH_PW'] : null;
// Allow to use the current session if a CSRF token is provided
if ($this->request->passesCSRFCheck()) {
if ($this->csrfValidator->validate($this->request)) {
return;
}
// Skip CORS check for requests with AppAPI auth.
@@ -43,6 +43,7 @@ use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\CSRF\ICsrfValidator;
use OCP\Security\Ip\IRemoteAddress;
use OCP\Util;
use Psr\Log\LoggerInterface;
@@ -73,6 +74,7 @@ class SecurityMiddleware extends Middleware {
private AuthorizedGroupMapper $groupAuthorizationMapper,
private IUserSession $userSession,
private IRemoteAddress $remoteAddress,
private readonly ICsrfValidator $csrfValidator,
) {
}
@@ -228,7 +230,7 @@ class SecurityMiddleware extends Middleware {
return false;
}
return !$this->request->passesCSRFCheck();
return !$this->csrfValidator->validate($this->request);
}
private function isValidOCSRequest(): bool {
+3 -1
View File
@@ -10,6 +10,7 @@ namespace OC;
use OCP\IEventSource;
use OCP\IRequest;
use OCP\Security\CSRF\ICsrfValidator;
class EventSource implements IEventSource {
private bool $fallback = false;
@@ -18,6 +19,7 @@ class EventSource implements IEventSource {
public function __construct(
private IRequest $request,
private ICsrfValidator $csrfValidator,
) {
}
@@ -54,7 +56,7 @@ class EventSource implements IEventSource {
header('Location: ' . \OC::$WEBROOT);
exit();
}
if (!$this->request->passesCSRFCheck()) {
if (!$this->csrfValidator->validate($this->request)) {
$this->send('error', 'Possible CSRF attack. Connection will be closed.');
$this->close();
exit();
+6 -1
View File
@@ -12,14 +12,19 @@ namespace OC;
use OCP\IEventSource;
use OCP\IEventSourceFactory;
use OCP\IRequest;
use OCP\Security\CSRF\ICsrfValidator;
class EventSourceFactory implements IEventSourceFactory {
public function __construct(
private IRequest $request,
private ICsrfValidator $csrfValidator,
) {
}
public function create(): IEventSource {
return new EventSource($this->request);
return new EventSource(
$this->request,
$this->csrfValidator,
);
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\CSRF;
use OCP\IRequest;
use OCP\Security\CSRF\ICsrfValidator;
class CsrfValidator implements ICsrfValidator {
public function __construct(
private CsrfTokenManager $csrfTokenManager
) {
}
public function validate(IRequest $request): bool {
if (!$request->passesStrictCookieCheck()) {
return false;
}
if ($request->getHeader('OCS-APIRequest') !== '') {
return true;
}
$token = $request->getParam('requesttoken', '');
if ($token === '') {
$token = $request->getHeader('REQUESTTOKEN');
}
if ($token === '') {
return false;
}
$token = new CsrfToken($token);
return $this->csrfTokenManager->isTokenValid($token);
}
}
+5
View File
@@ -101,6 +101,7 @@ use OC\Security\Crypto;
use OC\Security\CSP\ContentSecurityPolicyManager;
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\CSRF\CsrfValidator;
use OC\Security\CSRF\TokenStorage\SessionStorage;
use OC\Security\Hasher;
use OC\Security\Ip\RemoteAddress;
@@ -209,6 +210,8 @@ use OCP\RichObjectStrings\IRichTextFormatter;
use OCP\RichObjectStrings\IValidator;
use OCP\Route\IRouter;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\CSRF\ICsrfValidator;
use OCP\Security\IContentSecurityPolicyManager;
use OCP\Security\ICredentialsManager;
use OCP\Security\ICrypto;
use OCP\Security\IHasher;
@@ -1245,6 +1248,8 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(ISignatureManager::class, SignatureManager::class);
$this->registerAlias(ICsrfValidator::class, CsrfValidator::class);
$this->connectDispatcher();
}
+8 -2
View File
@@ -7,6 +7,8 @@
*/
use OC\Authentication\TwoFactorAuth\Manager as TwoFactorAuthManager;
use OCP\IRequest;
use OCP\Security\CSRF\ICsrfValidator;
class OC_JSON {
/**
@@ -45,12 +47,16 @@ class OC_JSON {
* @suppress PhanDeprecatedFunction
*/
public static function callCheck() {
if (!\OC::$server->getRequest()->passesStrictCookieCheck()) {
$request = OC::$server->get(IRequest::class);
if (!$request->passesStrictCookieCheck()) {
header('Location: ' . \OC::$WEBROOT);
exit();
}
if (!\OC::$server->getRequest()->passesCSRFCheck()) {
$csrfValidator = OC::$server->get(ICsrfValidator::class);
if (!$csrfValidator->validate($request)) {
$l = \OC::$server->getL10N('lib');
self::error([ 'data' => [ 'message' => $l->t('Token expired. Please reload page.'), 'error' => 'token_expired' ]]);
exit();
+1
View File
@@ -177,6 +177,7 @@ interface IRequest {
*
* @return bool true if CSRF check passed
* @since 6.0.0
* @deprecated 33.0.0 use \OCP\Security\CSRF\ICsrfValidator::validate instead
*/
public function passesCSRFCheck(): bool;
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace OCP\Security\CSRF;
use OCP\IRequest;
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
* @since 33.0.0
*/
interface ICsrfValidator {
/**
* Check if a request uses a valid csrf token.
*
* @since 33.0.0
*/
public function validate(IRequest $request): bool;
}
+69 -18
View File
@@ -15,6 +15,9 @@ use OC\Authentication\Login\LoginData;
use OC\Authentication\Login\LoginResult;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Core\Controller\LoginController;
use OC\Security\CSRF\CsrfToken;
use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\CSRF\CsrfValidator;
use OC\User\Session;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\RedirectResponse;
@@ -80,6 +83,9 @@ class LoginControllerTest extends TestCase {
/** @var IAppManager|MockObject */
private $appManager;
private CsrfTokenManager $csrfTokenManager;
private CsrfValidator $csrfValidator;
protected function setUp(): void {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
@@ -102,6 +108,8 @@ class LoginControllerTest extends TestCase {
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
$this->csrfTokenManager = $this->createMock(CsrfTokenManager::class);
$this->csrfValidator = new CsrfValidator($this->csrfTokenManager);
$this->request->method('getRemoteAddress')
@@ -130,6 +138,7 @@ class LoginControllerTest extends TestCase {
$this->notificationManager,
$this->l,
$this->appManager,
$this->csrfValidator,
);
}
@@ -483,9 +492,16 @@ class LoginControllerTest extends TestCase {
$loginChain = $this->createMock(LoginChain::class);
$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
$this->request
->expects($this->once())
->method('passesCSRFCheck')
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(true);
$loginData = new LoginData(
$this->request,
@@ -520,9 +536,16 @@ class LoginControllerTest extends TestCase {
$loginChain = $this->createMock(LoginChain::class);
$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
$this->request
->expects($this->once())
->method('passesCSRFCheck')
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(true);
$loginData = new LoginData(
$this->request,
@@ -554,9 +577,16 @@ class LoginControllerTest extends TestCase {
$loginChain = $this->createMock(LoginChain::class);
$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
$this->request
->expects($this->once())
->method('passesCSRFCheck')
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(false);
$this->userSession
->method('isLoggedIn')
@@ -586,9 +616,16 @@ class LoginControllerTest extends TestCase {
$loginChain = $this->createMock(LoginChain::class);
$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
$this->request
->expects($this->once())
->method('passesCSRFCheck')
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(false);
$this->userSession
->method('isLoggedIn')
@@ -620,9 +657,16 @@ class LoginControllerTest extends TestCase {
$loginChain = $this->createMock(LoginChain::class);
$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
$this->request
->expects($this->once())
->method('passesCSRFCheck')
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(true);
$loginData = new LoginData(
$this->request,
@@ -653,9 +697,16 @@ class LoginControllerTest extends TestCase {
$loginChain = $this->createMock(LoginChain::class);
$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
$this->request
->expects($this->once())
->method('passesCSRFCheck')
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(true);
$loginPageUrl = '/login?redirect_url=/apps/files';
$loginData = new LoginData(
@@ -12,6 +12,8 @@ use OC\AppFramework\Middleware\Security\CORSMiddleware;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\CSRF\CsrfValidator;
use OC\User\Session;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
@@ -30,6 +32,8 @@ class CORSMiddlewareTest extends \Test\TestCase {
private $session;
/** @var IThrottler|MockObject */
private $throttler;
private CsrfTokenManager $csrfTokenManager;
private CsrfValidator $csrfValidator;
/** @var CORSMiddlewareController */
private $controller;
private LoggerInterface $logger;
@@ -40,6 +44,8 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->session = $this->createMock(Session::class);
$this->throttler = $this->createMock(IThrottler::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->csrfTokenManager = $this->createMock(CsrfTokenManager::class);
$this->csrfValidator = new CsrfValidator($this->csrfTokenManager);
$this->controller = new CORSMiddlewareController(
'test',
$this->createMock(IRequest::class)
@@ -65,7 +71,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IConfig::class)
);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$response = $middleware->afterController($this->controller, $method, new Response());
$headers = $response->getHeaders();
@@ -82,7 +88,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IRequestId::class),
$this->createMock(IConfig::class)
);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$response = $middleware->afterController($this->controller, __FUNCTION__, new Response());
$headers = $response->getHeaders();
@@ -104,7 +110,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IConfig::class)
);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$response = $middleware->afterController($this->controller, $method, new Response());
$headers = $response->getHeaders();
@@ -132,7 +138,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IConfig::class)
);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$response = new Response();
$response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
@@ -156,7 +162,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IConfig::class)
);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$this->session->expects($this->once())
->method('isLoggedIn')
->willReturn(false);
@@ -188,7 +194,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IConfig::class)
);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$this->session->expects($this->once())
->method('isLoggedIn')
->willReturn(true);
@@ -227,7 +233,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->willReturn(true);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$middleware->beforeController($this->controller, $method);
}
@@ -258,7 +264,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->willThrowException(new PasswordLoginForbiddenException);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$middleware->beforeController($this->controller, $method);
}
@@ -289,7 +295,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->willReturn(false);
$this->reflector->reflect($this->controller, $method);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$middleware->beforeController($this->controller, $method);
}
@@ -303,7 +309,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IRequestId::class),
$this->createMock(IConfig::class)
);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception'));
$expected = new JSONResponse(['message' => 'A security exception'], 500);
@@ -319,7 +325,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IRequestId::class),
$this->createMock(IConfig::class)
);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501));
$expected = new JSONResponse(['message' => 'A security exception'], 501);
@@ -338,7 +344,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->createMock(IRequestId::class),
$this->createMock(IConfig::class)
);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
$middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger, $this->csrfValidator);
$middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception'));
}
}
@@ -19,6 +19,9 @@ use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Middleware\Security\SecurityMiddleware;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Security\CSRF\CsrfToken;
use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\CSRF\CsrfValidator;
use OC\Settings\AuthorizedGroupMapper;
use OC\User\Session;
use OCP\App\IAppManager;
@@ -69,6 +72,8 @@ class SecurityMiddlewareTest extends \Test\TestCase {
private $userSession;
/** @var AuthorizedGroupMapper|\PHPUnit\Framework\MockObject\MockObject */
private $authorizedGroupMapper;
private CsrfTokenManager $csrfTokenManager;
private CsrfValidator $csrfValidator;
protected function setUp(): void {
parent::setUp();
@@ -88,6 +93,8 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->navigationManager = $this->createMock(INavigationManager::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->l10n = $this->createMock(IL10N::class);
$this->csrfTokenManager = $this->createMock(CsrfTokenManager::class);
$this->csrfValidator = new CsrfValidator($this->csrfTokenManager);
$this->middleware = $this->getMiddleware(true, true, false);
$this->secException = new SecurityException('hey', false);
$this->secAjaxException = new SecurityException('hey', true);
@@ -122,7 +129,8 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->l10n,
$this->authorizedGroupMapper,
$this->userSession,
$remoteIpAddress
$remoteIpAddress,
$this->csrfValidator,
);
}
@@ -323,12 +331,18 @@ class SecurityMiddlewareTest extends \Test\TestCase {
public function testCsrfCheck(string $method): void {
$this->expectException(CrossSiteRequestForgeryException::class);
$this->request->expects($this->once())
->method('passesCSRFCheck')
->willReturn(false);
$this->request->expects($this->once())
$this->request->expects($this->exactly(2))
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('');
$this->request->expects($this->once())
->method('getHeader')
->with('REQUESTTOKEN')
->willReturn('');
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
}
@@ -345,11 +359,20 @@ class SecurityMiddlewareTest extends \Test\TestCase {
#[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
public function testPassesCsrfCheck(string $method): void {
$this->request->expects($this->once())
->method('passesCSRFCheck')
$this->request->expects($this->exactly(2))
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->method('getParam')
->with('requesttoken', '')
->willReturn('');
$this->request->expects($this->once())
->method('getHeader')
->with('REQUESTTOKEN')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(true);
$this->reader->reflect($this->controller, $method);
@@ -360,12 +383,21 @@ class SecurityMiddlewareTest extends \Test\TestCase {
public function testFailCsrfCheck(string $method): void {
$this->expectException(CrossSiteRequestForgeryException::class);
$this->request->expects($this->once())
->method('passesCSRFCheck')
->willReturn(false);
$this->request->expects($this->once())
$this->request->expects($this->exactly(2))
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('');
$this->request->expects($this->once())
->method('getHeader')
->with('REQUESTTOKEN')
->willReturn('foobar');
$this->csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with(new CsrfToken('foobar'))
->willReturn(false);
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
@@ -380,6 +412,12 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->request->expects($this->once())
->method('passesStrictCookieCheck')
->willReturn(false);
$this->request->expects($this->never())
->method('getParam');
$this->request->expects($this->never())
->method('getHeader');
$this->csrfTokenManager->expects($this->never())
->method('isTokenValid');
$this->reader->reflect($this->controller, $method);
$this->middleware->beforeController($this->controller, $method);
@@ -431,6 +469,9 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->request
->method('getHeader')
->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) {
if ($header === 'REQUESTTOKEN') {
return '';
}
if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) {
return 'true';
}
@@ -439,9 +480,15 @@ class SecurityMiddlewareTest extends \Test\TestCase {
}
return '';
});
$this->request->expects($this->once())
$this->request->expects($this->exactly(2))
->method('passesStrictCookieCheck')
->willReturn(true);
$this->request->expects($this->once())
->method('getParam')
->with('requesttoken', '')
->willReturn('');
$this->csrfTokenManager->expects($this->never())
->method('isTokenValid');
$controller = new $controllerClass('test', $this->request);
+3 -1
View File
@@ -10,11 +10,13 @@ namespace Test;
use OC\EventSourceFactory;
use OCP\IEventSource;
use OCP\IRequest;
use OCP\Security\CSRF\ICsrfValidator;
class EventSourceFactoryTest extends TestCase {
public function testCreate(): void {
$request = $this->createMock(IRequest::class);
$factory = new EventSourceFactory($request);
$csrfValidator = $this->createMock(ICsrfValidator::class);
$factory = new EventSourceFactory($request, $csrfValidator);
$instance = $factory->create();
$this->assertInstanceOf(IEventSource::class, $instance);
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Security\CSRF;
use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\CSRF\CsrfValidator;
use OCP\IRequest;
use Test\TestCase;
class CsrfValidatorTest extends TestCase {
private CsrfTokenManager $csrfTokenManager;
private CsrfValidator $csrfValidator;
protected function setUp(): void {
parent::setUp();
$this->csrfTokenManager = $this->createMock(CsrfTokenManager::class);
$this->csrfValidator = new CsrfValidator($this->csrfTokenManager);
}
public function testFailStrictCookieCheck(): void {
$request = $this->createMock(IRequest::class);
$request->method('passesStrictCookieCheck')
->willReturn(false);
$this->assertFalse($this->csrfValidator->validate($request));
}
public function testFailMissingToken(): void {
$request = $this->createMock(IRequest::class);
$request->method('passesStrictCookieCheck')
->willReturn(true);
$request->method('getParam')
->with('requesttoken', '')
->willReturn('');
$request->method('getHeader')
->with('REQUESTTOKEN')
->willReturn('');
$this->assertFalse($this->csrfValidator->validate($request));
}
public function testFailInvalidToken(): void {
$request = $this->createMock(IRequest::class);
$request->method('passesStrictCookieCheck')
->willReturn(true);
$request->method('getParam')
->with('requesttoken', '')
->willReturn('token123');
$request->method('getHeader')
->with('REQUESTTOKEN')
->willReturn('');
$this->csrfTokenManager
->method('isTokenValid')
->willReturn(false);
$this->assertFalse($this->csrfValidator->validate($request));
}
public function testPass(): void {
$request = $this->createMock(IRequest::class);
$request->method('passesStrictCookieCheck')
->willReturn(true);
$request->method('getParam')
->with('requesttoken', '')
->willReturn('token123');
$request->method('getHeader')
->with('REQUESTTOKEN')
->willReturn('');
$this->csrfTokenManager
->method('isTokenValid')
->willReturn(true);
$this->assertTrue($this->csrfValidator->validate($request));
}
public function testPassWithOCSAPIRequestHeader(): void {
$request = $this->createMock(IRequest::class);
$request->method('passesStrictCookieCheck')
->willReturn(true);
$request->method('getHeader')
->with('OCS-APIRequest', '')
->willReturn('yes');
$this->assertTrue($this->csrfValidator->validate($request));
}
}