CORSMiddlewareTest.php 11 KB

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