Browse Source

Merge pull request #43712 from nextcloud/bugfix/noid/add-bruteforce-protection-trusted-servers

fix: Add bruteforce protection to federation endpoint
tags/v29.0.0beta1
Joas Schilling 3 months ago
parent
commit
7ff81838b0
No account linked to committer's email address

+ 12
- 1
apps/federation/lib/Controller/OCSAuthAPIController.php View File

use OCP\AppFramework\Utility\ITimeFactory; use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\IJobList;
use OCP\IRequest; use OCP\IRequest;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ISecureRandom; use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;


private DbHandler $dbHandler; private DbHandler $dbHandler;
private LoggerInterface $logger; private LoggerInterface $logger;
private ITimeFactory $timeFactory; private ITimeFactory $timeFactory;
private IThrottler $throttler;


public function __construct( public function __construct(
string $appName, string $appName,
TrustedServers $trustedServers, TrustedServers $trustedServers,
DbHandler $dbHandler, DbHandler $dbHandler,
LoggerInterface $logger, LoggerInterface $logger,
ITimeFactory $timeFactory
ITimeFactory $timeFactory,
IThrottler $throttler
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);


$this->dbHandler = $dbHandler; $this->dbHandler = $dbHandler;
$this->logger = $logger; $this->logger = $logger;
$this->timeFactory = $timeFactory; $this->timeFactory = $timeFactory;
$this->throttler = $throttler;
} }


/** /**
* *
* @NoCSRFRequired * @NoCSRFRequired
* @PublicPage * @PublicPage
* @BruteForceProtection(action=federationSharedSecret)
* *
* @param string $url URL of the server * @param string $url URL of the server
* @param string $token Token of the server * @param string $token Token of the server
* *
* @NoCSRFRequired * @NoCSRFRequired
* @PublicPage * @PublicPage
* @BruteForceProtection(action=federationSharedSecret)
* *
* @param string $url URL of the server * @param string $url URL of the server
* @param string $token Token of the server * @param string $token Token of the server
* *
* @NoCSRFRequired * @NoCSRFRequired
* @PublicPage * @PublicPage
* @BruteForceProtection(action=federationSharedSecret)
* *
* @param string $url URL of the server * @param string $url URL of the server
* @param string $token Token of the server * @param string $token Token of the server
*/ */
public function requestSharedSecret(string $url, string $token): DataResponse { public function requestSharedSecret(string $url, string $token): DataResponse {
if ($this->trustedServers->isTrustedServer($url) === false) { if ($this->trustedServers->isTrustedServer($url) === false) {
$this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
$this->logger->error('remote server not trusted (' . $url . ') while requesting shared secret', ['app' => 'federation']); $this->logger->error('remote server not trusted (' . $url . ') while requesting shared secret', ['app' => 'federation']);
throw new OCSForbiddenException(); throw new OCSForbiddenException();
} }
* *
* @NoCSRFRequired * @NoCSRFRequired
* @PublicPage * @PublicPage
* @BruteForceProtection(action=federationSharedSecret)
* *
* @param string $url URL of the server * @param string $url URL of the server
* @param string $token Token of the server * @param string $token Token of the server
*/ */
public function getSharedSecret(string $url, string $token): DataResponse { public function getSharedSecret(string $url, string $token): DataResponse {
if ($this->trustedServers->isTrustedServer($url) === false) { if ($this->trustedServers->isTrustedServer($url) === false) {
$this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
$this->logger->error('remote server not trusted (' . $url . ') while getting shared secret', ['app' => 'federation']); $this->logger->error('remote server not trusted (' . $url . ') while getting shared secret', ['app' => 'federation']);
throw new OCSForbiddenException(); throw new OCSForbiddenException();
} }


if ($this->isValidToken($url, $token) === false) { if ($this->isValidToken($url, $token) === false) {
$this->throttler->registerAttempt('federationSharedSecret', $this->request->getRemoteAddress());
$expectedToken = $this->dbHandler->getToken($url); $expectedToken = $this->dbHandler->getToken($url);
$this->logger->error( $this->logger->error(
'remote server (' . $url . ') didn\'t send a valid token (got "' . $token . '" but expected "'. $expectedToken . '") while getting shared secret', 'remote server (' . $url . ') didn\'t send a valid token (got "' . $token . '" but expected "'. $expectedToken . '") while getting shared secret',

+ 18
- 2
apps/federation/tests/Controller/OCSAuthAPIControllerTest.php View File

use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\Utility\ITimeFactory; use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IRequest; use OCP\IRequest;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ISecureRandom; use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Test\TestCase; use Test\TestCase;
/** @var \PHPUnit\Framework\MockObject\MockObject|ITimeFactory */ /** @var \PHPUnit\Framework\MockObject\MockObject|ITimeFactory */
private $timeFactory; private $timeFactory;


/** @var \PHPUnit\Framework\MockObject\MockObject|IThrottler */
private $throttler;

private OCSAuthAPIController $ocsAuthApi; private OCSAuthAPIController $ocsAuthApi;


/** @var int simulated timestamp */ /** @var int simulated timestamp */
$this->jobList = $this->createMock(JobList::class); $this->jobList = $this->createMock(JobList::class);
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->timeFactory = $this->createMock(ITimeFactory::class); $this->timeFactory = $this->createMock(ITimeFactory::class);
$this->throttler = $this->createMock(IThrottler::class);


$this->ocsAuthApi = new OCSAuthAPIController( $this->ocsAuthApi = new OCSAuthAPIController(
'federation', 'federation',
$this->trustedServers, $this->trustedServers,
$this->dbHandler, $this->dbHandler,
$this->logger, $this->logger,
$this->timeFactory
$this->timeFactory,
$this->throttler
); );


$this->timeFactory->method('getTime') $this->timeFactory->method('getTime')
} else { } else {
$this->jobList->expects($this->never())->method('add'); $this->jobList->expects($this->never())->method('add');
$this->jobList->expects($this->never())->method('remove'); $this->jobList->expects($this->never())->method('remove');
if (!$isTrustedServer) {
$this->throttler->expects($this->once())
->method('registerAttempt')
->with('federationSharedSecret');
}
} }



try { try {
$this->ocsAuthApi->requestSharedSecret($url, $token); $this->ocsAuthApi->requestSharedSecret($url, $token);
$this->assertTrue($ok); $this->assertTrue($ok);
$this->trustedServers, $this->trustedServers,
$this->dbHandler, $this->dbHandler,
$this->logger, $this->logger,
$this->timeFactory
$this->timeFactory,
$this->throttler
] ]
)->setMethods(['isValidToken'])->getMock(); )->setMethods(['isValidToken'])->getMock();


} else { } else {
$this->secureRandom->expects($this->never())->method('generate'); $this->secureRandom->expects($this->never())->method('generate');
$this->trustedServers->expects($this->never())->method('addSharedSecret'); $this->trustedServers->expects($this->never())->method('addSharedSecret');
$this->throttler->expects($this->once())
->method('registerAttempt')
->with('federationSharedSecret');
} }


try { try {

Loading…
Cancel
Save