CORSMiddlewareTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2023 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace Test\AppFramework\Middleware\Security;
  8. use OC\AppFramework\Http\Request;
  9. use OC\AppFramework\Middleware\Security\CORSMiddleware;
  10. use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
  11. use OC\AppFramework\Utility\ControllerMethodReflector;
  12. use OC\User\Session;
  13. use OCP\AppFramework\Http\JSONResponse;
  14. use OCP\AppFramework\Http\Response;
  15. use OCP\IConfig;
  16. use OCP\IRequest;
  17. use OCP\IRequestId;
  18. use OCP\Security\Bruteforce\IThrottler;
  19. use PHPUnit\Framework\MockObject\MockObject;
  20. use Psr\Log\LoggerInterface;
  21. use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController;
  22. class CORSMiddlewareTest extends \Test\TestCase {
  23. /** @var ControllerMethodReflector */
  24. private $reflector;
  25. /** @var Session|MockObject */
  26. private $session;
  27. /** @var IThrottler|MockObject */
  28. private $throttler;
  29. /** @var CORSMiddlewareController */
  30. private $controller;
  31. private LoggerInterface $logger;
  32. protected function setUp(): void {
  33. parent::setUp();
  34. $this->reflector = new ControllerMethodReflector();
  35. $this->session = $this->createMock(Session::class);
  36. $this->throttler = $this->createMock(IThrottler::class);
  37. $this->logger = $this->createMock(LoggerInterface::class);
  38. $this->controller = new CORSMiddlewareController(
  39. 'test',
  40. $this->createMock(IRequest::class)
  41. );
  42. }
  43. public function dataSetCORSAPIHeader(): array {
  44. return [
  45. ['testSetCORSAPIHeader'],
  46. ['testSetCORSAPIHeaderAttribute'],
  47. ];
  48. }
  49. /**
  50. * @dataProvider dataSetCORSAPIHeader
  51. */
  52. public function testSetCORSAPIHeader(string $method): void {
  53. $request = new Request(
  54. [
  55. 'server' => [
  56. 'HTTP_ORIGIN' => 'test'
  57. ]
  58. ],
  59. $this->createMock(IRequestId::class),
  60. $this->createMock(IConfig::class)
  61. );
  62. $this->reflector->reflect($this->controller, $method);
  63. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  64. $response = $middleware->afterController($this->controller, $method, new Response());
  65. $headers = $response->getHeaders();
  66. $this->assertEquals('test', $headers['Access-Control-Allow-Origin']);
  67. }
  68. public function testNoAnnotationNoCORSHEADER(): void {
  69. $request = new Request(
  70. [
  71. 'server' => [
  72. 'HTTP_ORIGIN' => 'test'
  73. ]
  74. ],
  75. $this->createMock(IRequestId::class),
  76. $this->createMock(IConfig::class)
  77. );
  78. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  79. $response = $middleware->afterController($this->controller, __FUNCTION__, new Response());
  80. $headers = $response->getHeaders();
  81. $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
  82. }
  83. public function dataNoOriginHeaderNoCORSHEADER(): array {
  84. return [
  85. ['testNoOriginHeaderNoCORSHEADER'],
  86. ['testNoOriginHeaderNoCORSHEADERAttribute'],
  87. ];
  88. }
  89. /**
  90. * @dataProvider dataNoOriginHeaderNoCORSHEADER
  91. */
  92. public function testNoOriginHeaderNoCORSHEADER(string $method): void {
  93. $request = new Request(
  94. [],
  95. $this->createMock(IRequestId::class),
  96. $this->createMock(IConfig::class)
  97. );
  98. $this->reflector->reflect($this->controller, $method);
  99. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  100. $response = $middleware->afterController($this->controller, $method, new Response());
  101. $headers = $response->getHeaders();
  102. $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
  103. }
  104. public function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array {
  105. return [
  106. ['testCorsIgnoredIfWithCredentialsHeaderPresent'],
  107. ['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'],
  108. ];
  109. }
  110. /**
  111. * @dataProvider dataCorsIgnoredIfWithCredentialsHeaderPresent
  112. */
  113. public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void {
  114. $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class);
  115. $request = new Request(
  116. [
  117. 'server' => [
  118. 'HTTP_ORIGIN' => 'test'
  119. ]
  120. ],
  121. $this->createMock(IRequestId::class),
  122. $this->createMock(IConfig::class)
  123. );
  124. $this->reflector->reflect($this->controller, $method);
  125. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  126. $response = new Response();
  127. $response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
  128. $middleware->afterController($this->controller, $method, $response);
  129. }
  130. public function dataNoCORSOnAnonymousPublicPage(): array {
  131. return [
  132. ['testNoCORSOnAnonymousPublicPage'],
  133. ['testNoCORSOnAnonymousPublicPageAttribute'],
  134. ['testNoCORSAttributeOnAnonymousPublicPage'],
  135. ['testNoCORSAttributeOnAnonymousPublicPageAttribute'],
  136. ];
  137. }
  138. /**
  139. * @dataProvider dataNoCORSOnAnonymousPublicPage
  140. */
  141. public function testNoCORSOnAnonymousPublicPage(string $method): void {
  142. $request = new Request(
  143. [],
  144. $this->createMock(IRequestId::class),
  145. $this->createMock(IConfig::class)
  146. );
  147. $this->reflector->reflect($this->controller, $method);
  148. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  149. $this->session->expects($this->once())
  150. ->method('isLoggedIn')
  151. ->willReturn(false);
  152. $this->session->expects($this->never())
  153. ->method('logout');
  154. $this->session->expects($this->never())
  155. ->method('logClientIn')
  156. ->with($this->equalTo('user'), $this->equalTo('pass'))
  157. ->willReturn(true);
  158. $this->reflector->reflect($this->controller, $method);
  159. $middleware->beforeController($this->controller, $method);
  160. }
  161. public function dataCORSShouldNeverAllowCookieAuth(): array {
  162. return [
  163. ['testCORSShouldNeverAllowCookieAuth'],
  164. ['testCORSShouldNeverAllowCookieAuthAttribute'],
  165. ['testCORSAttributeShouldNeverAllowCookieAuth'],
  166. ['testCORSAttributeShouldNeverAllowCookieAuthAttribute'],
  167. ];
  168. }
  169. /**
  170. * @dataProvider dataCORSShouldNeverAllowCookieAuth
  171. */
  172. public function testCORSShouldNeverAllowCookieAuth(string $method): void {
  173. $request = new Request(
  174. [],
  175. $this->createMock(IRequestId::class),
  176. $this->createMock(IConfig::class)
  177. );
  178. $this->reflector->reflect($this->controller, $method);
  179. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  180. $this->session->expects($this->once())
  181. ->method('isLoggedIn')
  182. ->willReturn(true);
  183. $this->session->expects($this->once())
  184. ->method('logout');
  185. $this->session->expects($this->never())
  186. ->method('logClientIn')
  187. ->with($this->equalTo('user'), $this->equalTo('pass'))
  188. ->willReturn(true);
  189. $this->expectException(SecurityException::class);
  190. $middleware->beforeController($this->controller, $method);
  191. }
  192. public function dataCORSShouldRelogin(): array {
  193. return [
  194. ['testCORSShouldRelogin'],
  195. ['testCORSAttributeShouldRelogin'],
  196. ];
  197. }
  198. /**
  199. * @dataProvider dataCORSShouldRelogin
  200. */
  201. public function testCORSShouldRelogin(string $method): void {
  202. $request = new Request(
  203. ['server' => [
  204. 'PHP_AUTH_USER' => 'user',
  205. 'PHP_AUTH_PW' => 'pass'
  206. ]],
  207. $this->createMock(IRequestId::class),
  208. $this->createMock(IConfig::class)
  209. );
  210. $this->session->expects($this->once())
  211. ->method('logout');
  212. $this->session->expects($this->once())
  213. ->method('logClientIn')
  214. ->with($this->equalTo('user'), $this->equalTo('pass'))
  215. ->willReturn(true);
  216. $this->reflector->reflect($this->controller, $method);
  217. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  218. $middleware->beforeController($this->controller, $method);
  219. }
  220. public function dataCORSShouldFailIfPasswordLoginIsForbidden(): array {
  221. return [
  222. ['testCORSShouldFailIfPasswordLoginIsForbidden'],
  223. ['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'],
  224. ];
  225. }
  226. /**
  227. * @dataProvider dataCORSShouldFailIfPasswordLoginIsForbidden
  228. */
  229. public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void {
  230. $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class);
  231. $request = new Request(
  232. ['server' => [
  233. 'PHP_AUTH_USER' => 'user',
  234. 'PHP_AUTH_PW' => 'pass'
  235. ]],
  236. $this->createMock(IRequestId::class),
  237. $this->createMock(IConfig::class)
  238. );
  239. $this->session->expects($this->once())
  240. ->method('logout');
  241. $this->session->expects($this->once())
  242. ->method('logClientIn')
  243. ->with($this->equalTo('user'), $this->equalTo('pass'))
  244. ->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException));
  245. $this->reflector->reflect($this->controller, $method);
  246. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  247. $middleware->beforeController($this->controller, $method);
  248. }
  249. public function dataCORSShouldNotAllowCookieAuth(): array {
  250. return [
  251. ['testCORSShouldNotAllowCookieAuth'],
  252. ['testCORSAttributeShouldNotAllowCookieAuth'],
  253. ];
  254. }
  255. /**
  256. * @dataProvider dataCORSShouldNotAllowCookieAuth
  257. */
  258. public function testCORSShouldNotAllowCookieAuth(string $method): void {
  259. $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\SecurityException::class);
  260. $request = new Request(
  261. ['server' => [
  262. 'PHP_AUTH_USER' => 'user',
  263. 'PHP_AUTH_PW' => 'pass'
  264. ]],
  265. $this->createMock(IRequestId::class),
  266. $this->createMock(IConfig::class)
  267. );
  268. $this->session->expects($this->once())
  269. ->method('logout');
  270. $this->session->expects($this->once())
  271. ->method('logClientIn')
  272. ->with($this->equalTo('user'), $this->equalTo('pass'))
  273. ->willReturn(false);
  274. $this->reflector->reflect($this->controller, $method);
  275. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  276. $middleware->beforeController($this->controller, $method);
  277. }
  278. public function testAfterExceptionWithSecurityExceptionNoStatus(): void {
  279. $request = new Request(
  280. ['server' => [
  281. 'PHP_AUTH_USER' => 'user',
  282. 'PHP_AUTH_PW' => 'pass'
  283. ]],
  284. $this->createMock(IRequestId::class),
  285. $this->createMock(IConfig::class)
  286. );
  287. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  288. $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception'));
  289. $expected = new JSONResponse(['message' => 'A security exception'], 500);
  290. $this->assertEquals($expected, $response);
  291. }
  292. public function testAfterExceptionWithSecurityExceptionWithStatus(): void {
  293. $request = new Request(
  294. ['server' => [
  295. 'PHP_AUTH_USER' => 'user',
  296. 'PHP_AUTH_PW' => 'pass'
  297. ]],
  298. $this->createMock(IRequestId::class),
  299. $this->createMock(IConfig::class)
  300. );
  301. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  302. $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501));
  303. $expected = new JSONResponse(['message' => 'A security exception'], 501);
  304. $this->assertEquals($expected, $response);
  305. }
  306. public function testAfterExceptionWithRegularException(): void {
  307. $this->expectException(\Exception::class);
  308. $this->expectExceptionMessage('A regular exception');
  309. $request = new Request(
  310. ['server' => [
  311. 'PHP_AUTH_USER' => 'user',
  312. 'PHP_AUTH_PW' => 'pass'
  313. ]],
  314. $this->createMock(IRequestId::class),
  315. $this->createMock(IConfig::class)
  316. );
  317. $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler, $this->logger);
  318. $middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception'));
  319. }
  320. }