SecurityMiddlewareTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. <?php
  2. /**
  3. * @author Bernhard Posselt <dev@bernhard-posselt.com>
  4. * @author Lukas Reschke <lukas@owncloud.com>
  5. *
  6. * @copyright Copyright (c) 2015, ownCloud, Inc.
  7. * @license AGPL-3.0
  8. *
  9. * This code is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License, version 3,
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>
  20. *
  21. */
  22. namespace Test\AppFramework\Middleware\Security;
  23. use OC\AppFramework\Http;
  24. use OC\AppFramework\Http\Request;
  25. use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
  26. use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
  27. use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
  28. use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
  29. use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
  30. use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException;
  31. use OC\AppFramework\Middleware\Security\SecurityMiddleware;
  32. use OC\AppFramework\Utility\ControllerMethodReflector;
  33. use OCP\App\IAppManager;
  34. use OCP\AppFramework\Controller;
  35. use OCP\AppFramework\Http\JSONResponse;
  36. use OCP\AppFramework\Http\RedirectResponse;
  37. use OCP\AppFramework\Http\TemplateResponse;
  38. use OCP\IConfig;
  39. use OCP\IL10N;
  40. use OCP\ILogger;
  41. use OCP\INavigationManager;
  42. use OCP\IRequest;
  43. use OCP\IURLGenerator;
  44. use OCP\Security\ISecureRandom;
  45. class SecurityMiddlewareTest extends \Test\TestCase {
  46. /** @var SecurityMiddleware|\PHPUnit\Framework\MockObject\MockObject */
  47. private $middleware;
  48. /** @var Controller|\PHPUnit\Framework\MockObject\MockObject */
  49. private $controller;
  50. /** @var SecurityException */
  51. private $secException;
  52. /** @var SecurityException */
  53. private $secAjaxException;
  54. /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
  55. private $request;
  56. /** @var ControllerMethodReflector */
  57. private $reader;
  58. /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
  59. private $logger;
  60. /** @var INavigationManager|\PHPUnit\Framework\MockObject\MockObject */
  61. private $navigationManager;
  62. /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
  63. private $urlGenerator;
  64. /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
  65. private $appManager;
  66. /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
  67. private $l10n;
  68. protected function setUp(): void {
  69. parent::setUp();
  70. $this->controller = $this->createMock(Controller::class);
  71. $this->reader = new ControllerMethodReflector();
  72. $this->logger = $this->createMock(ILogger::class);
  73. $this->navigationManager = $this->createMock(INavigationManager::class);
  74. $this->urlGenerator = $this->createMock(IURLGenerator::class);
  75. $this->request = $this->createMock(IRequest::class);
  76. $this->l10n = $this->createMock(IL10N::class);
  77. $this->middleware = $this->getMiddleware(true, true, false);
  78. $this->secException = new SecurityException('hey', false);
  79. $this->secAjaxException = new SecurityException('hey', true);
  80. }
  81. private function getMiddleware(bool $isLoggedIn, bool $isAdminUser, bool $isSubAdmin, bool $isAppEnabledForUser = true): SecurityMiddleware {
  82. $this->appManager = $this->createMock(IAppManager::class);
  83. $this->appManager->expects($this->any())
  84. ->method('isEnabledForUser')
  85. ->willReturn($isAppEnabledForUser);
  86. return new SecurityMiddleware(
  87. $this->request,
  88. $this->reader,
  89. $this->navigationManager,
  90. $this->urlGenerator,
  91. $this->logger,
  92. 'files',
  93. $isLoggedIn,
  94. $isAdminUser,
  95. $isSubAdmin,
  96. $this->appManager,
  97. $this->l10n
  98. );
  99. }
  100. /**
  101. * @PublicPage
  102. * @NoCSRFRequired
  103. */
  104. public function testSetNavigationEntry() {
  105. $this->navigationManager->expects($this->once())
  106. ->method('setActiveEntry')
  107. ->with($this->equalTo('files'));
  108. $this->reader->reflect(__CLASS__, __FUNCTION__);
  109. $this->middleware->beforeController($this->controller, __FUNCTION__);
  110. }
  111. /**
  112. * @param string $method
  113. * @param string $test
  114. */
  115. private function ajaxExceptionStatus($method, $test, $status) {
  116. $isLoggedIn = false;
  117. $isAdminUser = false;
  118. // isAdminUser requires isLoggedIn call to return true
  119. if ($test === 'isAdminUser') {
  120. $isLoggedIn = true;
  121. }
  122. $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
  123. try {
  124. $this->reader->reflect(__CLASS__, $method);
  125. $sec->beforeController($this->controller, $method);
  126. } catch (SecurityException $ex) {
  127. $this->assertEquals($status, $ex->getCode());
  128. }
  129. // add assertion if everything should work fine otherwise phpunit will
  130. // complain
  131. if ($status === 0) {
  132. $this->addToAssertionCount(1);
  133. }
  134. }
  135. public function testAjaxStatusLoggedInCheck() {
  136. $this->ajaxExceptionStatus(
  137. __FUNCTION__,
  138. 'isLoggedIn',
  139. Http::STATUS_UNAUTHORIZED
  140. );
  141. }
  142. /**
  143. * @NoCSRFRequired
  144. */
  145. public function testAjaxNotAdminCheck() {
  146. $this->ajaxExceptionStatus(
  147. __FUNCTION__,
  148. 'isAdminUser',
  149. Http::STATUS_FORBIDDEN
  150. );
  151. }
  152. /**
  153. * @PublicPage
  154. */
  155. public function testAjaxStatusCSRFCheck() {
  156. $this->ajaxExceptionStatus(
  157. __FUNCTION__,
  158. 'passesCSRFCheck',
  159. Http::STATUS_PRECONDITION_FAILED
  160. );
  161. }
  162. /**
  163. * @PublicPage
  164. * @NoCSRFRequired
  165. */
  166. public function testAjaxStatusAllGood() {
  167. $this->ajaxExceptionStatus(
  168. __FUNCTION__,
  169. 'isLoggedIn',
  170. 0
  171. );
  172. $this->ajaxExceptionStatus(
  173. __FUNCTION__,
  174. 'isAdminUser',
  175. 0
  176. );
  177. $this->ajaxExceptionStatus(
  178. __FUNCTION__,
  179. 'passesCSRFCheck',
  180. 0
  181. );
  182. }
  183. /**
  184. * @PublicPage
  185. * @NoCSRFRequired
  186. */
  187. public function testNoChecks() {
  188. $this->request->expects($this->never())
  189. ->method('passesCSRFCheck')
  190. ->willReturn(false);
  191. $sec = $this->getMiddleware(false, false, false);
  192. $this->reader->reflect(__CLASS__, __FUNCTION__);
  193. $sec->beforeController($this->controller, __FUNCTION__);
  194. }
  195. /**
  196. * @param string $method
  197. * @param string $expects
  198. */
  199. private function securityCheck($method, $expects, $shouldFail=false) {
  200. // admin check requires login
  201. if ($expects === 'isAdminUser') {
  202. $isLoggedIn = true;
  203. $isAdminUser = !$shouldFail;
  204. } else {
  205. $isLoggedIn = !$shouldFail;
  206. $isAdminUser = false;
  207. }
  208. $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
  209. if ($shouldFail) {
  210. $this->expectException(SecurityException::class);
  211. } else {
  212. $this->addToAssertionCount(1);
  213. }
  214. $this->reader->reflect(__CLASS__, $method);
  215. $sec->beforeController($this->controller, $method);
  216. }
  217. /**
  218. * @PublicPage
  219. */
  220. public function testCsrfCheck() {
  221. $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class);
  222. $this->request->expects($this->once())
  223. ->method('passesCSRFCheck')
  224. ->willReturn(false);
  225. $this->request->expects($this->once())
  226. ->method('passesStrictCookieCheck')
  227. ->willReturn(true);
  228. $this->reader->reflect(__CLASS__, __FUNCTION__);
  229. $this->middleware->beforeController($this->controller, __FUNCTION__);
  230. }
  231. /**
  232. * @PublicPage
  233. * @NoCSRFRequired
  234. */
  235. public function testNoCsrfCheck() {
  236. $this->request->expects($this->never())
  237. ->method('passesCSRFCheck')
  238. ->willReturn(false);
  239. $this->reader->reflect(__CLASS__, __FUNCTION__);
  240. $this->middleware->beforeController($this->controller, __FUNCTION__);
  241. }
  242. /**
  243. * @PublicPage
  244. */
  245. public function testPassesCsrfCheck() {
  246. $this->request->expects($this->once())
  247. ->method('passesCSRFCheck')
  248. ->willReturn(true);
  249. $this->request->expects($this->once())
  250. ->method('passesStrictCookieCheck')
  251. ->willReturn(true);
  252. $this->reader->reflect(__CLASS__, __FUNCTION__);
  253. $this->middleware->beforeController($this->controller, __FUNCTION__);
  254. }
  255. /**
  256. * @PublicPage
  257. */
  258. public function testFailCsrfCheck() {
  259. $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException::class);
  260. $this->request->expects($this->once())
  261. ->method('passesCSRFCheck')
  262. ->willReturn(false);
  263. $this->request->expects($this->once())
  264. ->method('passesStrictCookieCheck')
  265. ->willReturn(true);
  266. $this->reader->reflect(__CLASS__, __FUNCTION__);
  267. $this->middleware->beforeController($this->controller, __FUNCTION__);
  268. }
  269. /**
  270. * @PublicPage
  271. * @StrictCookieRequired
  272. */
  273. public function testStrictCookieRequiredCheck() {
  274. $this->expectException(\OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException::class);
  275. $this->request->expects($this->never())
  276. ->method('passesCSRFCheck');
  277. $this->request->expects($this->once())
  278. ->method('passesStrictCookieCheck')
  279. ->willReturn(false);
  280. $this->reader->reflect(__CLASS__, __FUNCTION__);
  281. $this->middleware->beforeController($this->controller, __FUNCTION__);
  282. }
  283. /**
  284. * @PublicPage
  285. * @NoCSRFRequired
  286. */
  287. public function testNoStrictCookieRequiredCheck() {
  288. $this->request->expects($this->never())
  289. ->method('passesStrictCookieCheck')
  290. ->willReturn(false);
  291. $this->reader->reflect(__CLASS__, __FUNCTION__);
  292. $this->middleware->beforeController($this->controller, __FUNCTION__);
  293. }
  294. /**
  295. * @PublicPage
  296. * @NoCSRFRequired
  297. * @StrictCookieRequired
  298. */
  299. public function testPassesStrictCookieRequiredCheck() {
  300. $this->request
  301. ->expects($this->once())
  302. ->method('passesStrictCookieCheck')
  303. ->willReturn(true);
  304. $this->reader->reflect(__CLASS__, __FUNCTION__);
  305. $this->middleware->beforeController($this->controller, __FUNCTION__);
  306. }
  307. public function dataCsrfOcsController() {
  308. $controller = $this->getMockBuilder('OCP\AppFramework\Controller')
  309. ->disableOriginalConstructor()
  310. ->getMock();
  311. $ocsController = $this->getMockBuilder('OCP\AppFramework\OCSController')
  312. ->disableOriginalConstructor()
  313. ->getMock();
  314. return [
  315. [$controller, false, false, true],
  316. [$controller, false, true, true],
  317. [$controller, true, false, true],
  318. [$controller, true, true, true],
  319. [$ocsController, false, false, true],
  320. [$ocsController, false, true, false],
  321. [$ocsController, true, false, false],
  322. [$ocsController, true, true, false],
  323. ];
  324. }
  325. /**
  326. * @dataProvider dataCsrfOcsController
  327. * @param Controller $controller
  328. * @param bool $hasOcsApiHeader
  329. * @param bool $hasBearerAuth
  330. * @param bool $exception
  331. */
  332. public function testCsrfOcsController(Controller $controller, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception) {
  333. $this->request
  334. ->method('getHeader')
  335. ->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) {
  336. if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) {
  337. return 'true';
  338. }
  339. if ($header === 'Authorization' && $hasBearerAuth) {
  340. return 'Bearer TOKEN!';
  341. }
  342. return '';
  343. });
  344. $this->request->expects($this->once())
  345. ->method('passesStrictCookieCheck')
  346. ->willReturn(true);
  347. try {
  348. $this->middleware->beforeController($controller, 'foo');
  349. $this->assertFalse($exception);
  350. } catch (CrossSiteRequestForgeryException $e) {
  351. $this->assertTrue($exception);
  352. }
  353. }
  354. /**
  355. * @NoCSRFRequired
  356. * @NoAdminRequired
  357. */
  358. public function testLoggedInCheck() {
  359. $this->securityCheck(__FUNCTION__, 'isLoggedIn');
  360. }
  361. /**
  362. * @NoCSRFRequired
  363. * @NoAdminRequired
  364. */
  365. public function testFailLoggedInCheck() {
  366. $this->securityCheck(__FUNCTION__, 'isLoggedIn', true);
  367. }
  368. /**
  369. * @NoCSRFRequired
  370. */
  371. public function testIsAdminCheck() {
  372. $this->securityCheck(__FUNCTION__, 'isAdminUser');
  373. }
  374. /**
  375. * @NoCSRFRequired
  376. * @SubAdminRequired
  377. */
  378. public function testIsNotSubAdminCheck() {
  379. $this->reader->reflect(__CLASS__,__FUNCTION__);
  380. $sec = $this->getMiddleware(true, false, false);
  381. $this->expectException(SecurityException::class);
  382. $sec->beforeController($this, __METHOD__);
  383. }
  384. /**
  385. * @NoCSRFRequired
  386. * @SubAdminRequired
  387. */
  388. public function testIsSubAdminCheck() {
  389. $this->reader->reflect(__CLASS__,__FUNCTION__);
  390. $sec = $this->getMiddleware(true, false, true);
  391. $sec->beforeController($this, __METHOD__);
  392. $this->addToAssertionCount(1);
  393. }
  394. /**
  395. * @NoCSRFRequired
  396. * @SubAdminRequired
  397. */
  398. public function testIsSubAdminAndAdminCheck() {
  399. $this->reader->reflect(__CLASS__,__FUNCTION__);
  400. $sec = $this->getMiddleware(true, true, true);
  401. $sec->beforeController($this, __METHOD__);
  402. $this->addToAssertionCount(1);
  403. }
  404. /**
  405. * @NoCSRFRequired
  406. */
  407. public function testFailIsAdminCheck() {
  408. $this->securityCheck(__FUNCTION__, 'isAdminUser', true);
  409. }
  410. public function testAfterExceptionNotCaughtThrowsItAgain() {
  411. $ex = new \Exception();
  412. $this->expectException(\Exception::class);
  413. $this->middleware->afterException($this->controller, 'test', $ex);
  414. }
  415. public function testAfterExceptionReturnsRedirectForNotLoggedInUser() {
  416. $this->request = new Request(
  417. [
  418. 'server' =>
  419. [
  420. 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  421. 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
  422. ]
  423. ],
  424. $this->createMock(ISecureRandom::class),
  425. $this->createMock(IConfig::class)
  426. );
  427. $this->middleware = $this->getMiddleware(false, false, false);
  428. $this->urlGenerator
  429. ->expects($this->once())
  430. ->method('linkToRoute')
  431. ->with(
  432. 'core.login.showLoginForm',
  433. [
  434. 'redirect_url' => 'nextcloud/index.php/apps/specialapp',
  435. ]
  436. )
  437. ->willReturn('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
  438. $this->logger
  439. ->expects($this->once())
  440. ->method('logException');
  441. $response = $this->middleware->afterException(
  442. $this->controller,
  443. 'test',
  444. new NotLoggedInException()
  445. );
  446. $expected = new RedirectResponse('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
  447. $this->assertEquals($expected , $response);
  448. }
  449. public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail() {
  450. $this->request = new Request(
  451. [
  452. 'server' => [
  453. 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  454. 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp',
  455. ],
  456. ],
  457. $this->createMock(ISecureRandom::class),
  458. $this->createMock(IConfig::class)
  459. );
  460. $this->middleware = $this->getMiddleware(false, false, false);
  461. $response = $this->middleware->afterException(
  462. $this->controller,
  463. 'test',
  464. new StrictCookieMissingException()
  465. );
  466. $expected = new RedirectResponse(\OC::$WEBROOT . '/');
  467. $this->assertEquals($expected , $response);
  468. }
  469. /**
  470. * @return array
  471. */
  472. public function exceptionProvider() {
  473. return [
  474. [
  475. new AppNotEnabledException(),
  476. ],
  477. [
  478. new CrossSiteRequestForgeryException(),
  479. ],
  480. [
  481. new NotAdminException(''),
  482. ],
  483. ];
  484. }
  485. /**
  486. * @dataProvider exceptionProvider
  487. * @param SecurityException $exception
  488. */
  489. public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception) {
  490. $this->request = new Request(
  491. [
  492. 'server' =>
  493. [
  494. 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  495. 'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
  496. ]
  497. ],
  498. $this->createMock(ISecureRandom::class),
  499. $this->createMock(IConfig::class)
  500. );
  501. $this->middleware = $this->getMiddleware(false, false, false);
  502. $this->logger
  503. ->expects($this->once())
  504. ->method('logException');
  505. $response = $this->middleware->afterException(
  506. $this->controller,
  507. 'test',
  508. $exception
  509. );
  510. $expected = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
  511. $expected->setStatus($exception->getCode());
  512. $this->assertEquals($expected , $response);
  513. }
  514. public function testAfterAjaxExceptionReturnsJSONError() {
  515. $response = $this->middleware->afterException($this->controller, 'test',
  516. $this->secAjaxException);
  517. $this->assertTrue($response instanceof JSONResponse);
  518. }
  519. public function dataRestrictedApp() {
  520. return [
  521. [false, false, false,],
  522. [false, false, true,],
  523. [false, true, false,],
  524. [false, true, true,],
  525. [ true, false, false,],
  526. [ true, false, true,],
  527. [ true, true, false,],
  528. [ true, true, true,],
  529. ];
  530. }
  531. /**
  532. * @PublicPage
  533. * @NoAdminRequired
  534. * @NoCSRFRequired
  535. */
  536. public function testRestrictedAppLoggedInPublicPage() {
  537. $middleware = $this->getMiddleware(true, false, false);
  538. $this->reader->reflect(__CLASS__,__FUNCTION__);
  539. $this->appManager->method('getAppPath')
  540. ->with('files')
  541. ->willReturn('foo');
  542. $this->appManager->method('isEnabledForUser')
  543. ->with('files')
  544. ->willReturn(false);
  545. $middleware->beforeController($this->controller, __FUNCTION__);
  546. $this->addToAssertionCount(1);
  547. }
  548. /**
  549. * @PublicPage
  550. * @NoAdminRequired
  551. * @NoCSRFRequired
  552. */
  553. public function testRestrictedAppNotLoggedInPublicPage() {
  554. $middleware = $this->getMiddleware(false, false, false);
  555. $this->reader->reflect(__CLASS__,__FUNCTION__);
  556. $this->appManager->method('getAppPath')
  557. ->with('files')
  558. ->willReturn('foo');
  559. $this->appManager->method('isEnabledForUser')
  560. ->with('files')
  561. ->willReturn(false);
  562. $middleware->beforeController($this->controller, __FUNCTION__);
  563. $this->addToAssertionCount(1);
  564. }
  565. /**
  566. * @NoAdminRequired
  567. * @NoCSRFRequired
  568. */
  569. public function testRestrictedAppLoggedIn() {
  570. $middleware = $this->getMiddleware(true, false, false, false);
  571. $this->reader->reflect(__CLASS__,__FUNCTION__);
  572. $this->appManager->method('getAppPath')
  573. ->with('files')
  574. ->willReturn('foo');
  575. $this->expectException(AppNotEnabledException::class);
  576. $middleware->beforeController($this->controller, __FUNCTION__);
  577. }
  578. }