You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SecurityHeadersTest.php 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\Settings\Tests;
  8. use OCA\Settings\SetupChecks\SecurityHeaders;
  9. use OCP\Http\Client\IClientService;
  10. use OCP\Http\Client\IResponse;
  11. use OCP\IConfig;
  12. use OCP\IL10N;
  13. use OCP\IURLGenerator;
  14. use OCP\SetupCheck\SetupResult;
  15. use PHPUnit\Framework\MockObject\MockObject;
  16. use Psr\Log\LoggerInterface;
  17. use Test\TestCase;
  18. class SecurityHeadersTest extends TestCase {
  19. private IL10N|MockObject $l10n;
  20. private IConfig|MockObject $config;
  21. private IURLGenerator|MockObject $urlGenerator;
  22. private IClientService|MockObject $clientService;
  23. private LoggerInterface|MockObject $logger;
  24. private SecurityHeaders|MockObject $setupcheck;
  25. protected function setUp(): void {
  26. parent::setUp();
  27. /** @var IL10N|MockObject */
  28. $this->l10n = $this->getMockBuilder(IL10N::class)
  29. ->disableOriginalConstructor()->getMock();
  30. $this->l10n->expects($this->any())
  31. ->method('t')
  32. ->willReturnCallback(function ($message, array $replace) {
  33. return vsprintf($message, $replace);
  34. });
  35. $this->config = $this->createMock(IConfig::class);
  36. $this->urlGenerator = $this->createMock(IURLGenerator::class);
  37. $this->clientService = $this->createMock(IClientService::class);
  38. $this->logger = $this->createMock(LoggerInterface::class);
  39. $this->setupcheck = $this->getMockBuilder(SecurityHeaders::class)
  40. ->onlyMethods(['runRequest'])
  41. ->setConstructorArgs([
  42. $this->l10n,
  43. $this->config,
  44. $this->urlGenerator,
  45. $this->clientService,
  46. $this->logger,
  47. ])
  48. ->getMock();
  49. }
  50. public function testInvalidStatusCode(): void {
  51. $this->setupResponse(500, []);
  52. $result = $this->setupcheck->run();
  53. $this->assertMatchesRegularExpression('/^Could not check that your web server serves security headers correctly/', $result->getDescription());
  54. $this->assertEquals(SetupResult::WARNING, $result->getSeverity());
  55. }
  56. public function testAllHeadersMissing(): void {
  57. $this->setupResponse(200, []);
  58. $result = $this->setupcheck->run();
  59. $this->assertMatchesRegularExpression('/^Some headers are not set correctly on your instance/', $result->getDescription());
  60. $this->assertEquals(SetupResult::WARNING, $result->getSeverity());
  61. }
  62. public function testSomeHeadersMissing(): void {
  63. $this->setupResponse(
  64. 200,
  65. [
  66. 'X-Robots-Tag' => 'noindex, nofollow',
  67. 'X-Frame-Options' => 'SAMEORIGIN',
  68. 'Strict-Transport-Security' => 'max-age=15768000;preload',
  69. 'X-Permitted-Cross-Domain-Policies' => 'none',
  70. 'Referrer-Policy' => 'no-referrer',
  71. ]
  72. );
  73. $result = $this->setupcheck->run();
  74. $this->assertEquals(
  75. "Some headers are not set correctly on your instance\n- The `X-Content-Type-Options` HTTP header is not set to `nosniff`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.\n- The `X-XSS-Protection` HTTP header does not contain `1; mode=block`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.\n",
  76. $result->getDescription()
  77. );
  78. $this->assertEquals(SetupResult::WARNING, $result->getSeverity());
  79. }
  80. public function dataSuccess(): array {
  81. return [
  82. // description => modifiedHeaders
  83. 'basic' => [[]],
  84. 'extra-xss-protection' => [['X-XSS-Protection' => '1; mode=block; report=https://example.com']],
  85. 'no-space-in-x-robots' => [['X-Robots-Tag' => 'noindex,nofollow']],
  86. 'strict-origin-when-cross-origin' => [['Referrer-Policy' => 'strict-origin-when-cross-origin']],
  87. 'referrer-no-referrer-when-downgrade' => [['Referrer-Policy' => 'no-referrer-when-downgrade']],
  88. 'referrer-strict-origin' => [['Referrer-Policy' => 'strict-origin']],
  89. 'referrer-strict-origin-when-cross-origin' => [['Referrer-Policy' => 'strict-origin-when-cross-origin']],
  90. 'referrer-same-origin' => [['Referrer-Policy' => 'same-origin']],
  91. 'hsts-minimum' => [['Strict-Transport-Security' => 'max-age=15552000']],
  92. 'hsts-include-subdomains' => [['Strict-Transport-Security' => 'max-age=99999999; includeSubDomains']],
  93. 'hsts-include-subdomains-preload' => [['Strict-Transport-Security' => 'max-age=99999999; preload; includeSubDomains']],
  94. ];
  95. }
  96. /**
  97. * @dataProvider dataSuccess
  98. */
  99. public function testSuccess($headers): void {
  100. $headers = array_merge(
  101. [
  102. 'X-XSS-Protection' => '1; mode=block',
  103. 'X-Content-Type-Options' => 'nosniff',
  104. 'X-Robots-Tag' => 'noindex, nofollow',
  105. 'X-Frame-Options' => 'SAMEORIGIN',
  106. 'Strict-Transport-Security' => 'max-age=15768000',
  107. 'X-Permitted-Cross-Domain-Policies' => 'none',
  108. 'Referrer-Policy' => 'no-referrer',
  109. ],
  110. $headers
  111. );
  112. $this->setupResponse(
  113. 200,
  114. $headers
  115. );
  116. $result = $this->setupcheck->run();
  117. $this->assertEquals(
  118. 'Your server is correctly configured to send security headers.',
  119. $result->getDescription()
  120. );
  121. $this->assertEquals(SetupResult::SUCCESS, $result->getSeverity());
  122. }
  123. public function dataFailure(): array {
  124. return [
  125. // description => modifiedHeaders
  126. 'x-robots-none' => [['X-Robots-Tag' => 'none'], "- The `X-Robots-Tag` HTTP header is not set to `noindex,nofollow`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.\n"],
  127. 'xss-protection-1' => [['X-XSS-Protection' => '1'], "- The `X-XSS-Protection` HTTP header does not contain `1; mode=block`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.\n"],
  128. 'xss-protection-0' => [['X-XSS-Protection' => '0'], "- The `X-XSS-Protection` HTTP header does not contain `1; mode=block`. This is a potential security or privacy risk, as it is recommended to adjust this setting accordingly.\n"],
  129. 'referrer-origin' => [['Referrer-Policy' => 'origin'], "- The `Referrer-Policy` HTTP header is not set to `no-referrer`, `no-referrer-when-downgrade`, `strict-origin`, `strict-origin-when-cross-origin` or `same-origin`. This can leak referer information. See the {w3c-recommendation}.\n"],
  130. 'referrer-origin-when-cross-origin' => [['Referrer-Policy' => 'origin-when-cross-origin'], "- The `Referrer-Policy` HTTP header is not set to `no-referrer`, `no-referrer-when-downgrade`, `strict-origin`, `strict-origin-when-cross-origin` or `same-origin`. This can leak referer information. See the {w3c-recommendation}.\n"],
  131. 'referrer-unsafe-url' => [['Referrer-Policy' => 'unsafe-url'], "- The `Referrer-Policy` HTTP header is not set to `no-referrer`, `no-referrer-when-downgrade`, `strict-origin`, `strict-origin-when-cross-origin` or `same-origin`. This can leak referer information. See the {w3c-recommendation}.\n"],
  132. 'hsts-missing' => [['Strict-Transport-Security' => ''], "- The `Strict-Transport-Security` HTTP header is not set (should be at least `15552000` seconds). For enhanced security, it is recommended to enable HSTS.\n"],
  133. 'hsts-too-low' => [['Strict-Transport-Security' => 'max-age=15551999'], "- The `Strict-Transport-Security` HTTP header is not set to at least `15552000` seconds (current value: `15551999`). For enhanced security, it is recommended to use a long HSTS policy.\n"],
  134. 'hsts-malformed' => [['Strict-Transport-Security' => 'iAmABogusHeader342'], "- The `Strict-Transport-Security` HTTP header is malformed: `iAmABogusHeader342`. For enhanced security, it is recommended to enable HSTS.\n"],
  135. ];
  136. }
  137. /**
  138. * @dataProvider dataFailure
  139. */
  140. public function testFailure(array $headers, string $msg): void {
  141. $headers = array_merge(
  142. [
  143. 'X-XSS-Protection' => '1; mode=block',
  144. 'X-Content-Type-Options' => 'nosniff',
  145. 'X-Robots-Tag' => 'noindex, nofollow',
  146. 'X-Frame-Options' => 'SAMEORIGIN',
  147. 'Strict-Transport-Security' => 'max-age=15768000',
  148. 'X-Permitted-Cross-Domain-Policies' => 'none',
  149. 'Referrer-Policy' => 'no-referrer',
  150. ],
  151. $headers
  152. );
  153. $this->setupResponse(
  154. 200,
  155. $headers
  156. );
  157. $result = $this->setupcheck->run();
  158. $this->assertEquals(
  159. 'Some headers are not set correctly on your instance'."\n$msg",
  160. $result->getDescription()
  161. );
  162. $this->assertEquals(SetupResult::WARNING, $result->getSeverity());
  163. }
  164. protected function setupResponse(int $statuscode, array $headers): void {
  165. $response = $this->createMock(IResponse::class);
  166. $response->expects($this->atLeastOnce())->method('getStatusCode')->willReturn($statuscode);
  167. $response->expects($this->any())->method('getHeader')
  168. ->willReturnCallback(
  169. fn (string $header): string => $headers[$header] ?? ''
  170. );
  171. $this->setupcheck
  172. ->expects($this->atLeastOnce())
  173. ->method('runRequest')
  174. ->willReturnOnConsecutiveCalls($this->generate([$response]));
  175. }
  176. /**
  177. * Helper function creates a nicer interface for mocking Generator behavior
  178. */
  179. protected function generate(array $yield_values) {
  180. return $this->returnCallback(function () use ($yield_values) {
  181. yield from $yield_values;
  182. });
  183. }
  184. }