‪TYPO3CMS  10.4
ServerResponseCheck.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use GuzzleHttp\Client;
21 use GuzzleHttp\Exception\BadResponseException;
22 use GuzzleHttp\Exception\TransferException;
23 use function GuzzleHttp\Promise\settle;
24 use Psr\Http\Message\ResponseInterface;
34 
42 {
43  protected const ‪WRAP_FLAT = 1;
44  protected const ‪WRAP_NESTED = 2;
45 
49  protected ‪$useMarkup;
50 
54  protected ‪$messageQueue;
55 
59  protected ‪$assetLocation;
60 
65 
69  protected ‪$fileDeclarations;
70 
71  public function ‪__construct(bool ‪$useMarkup = true)
72  {
73  $this->useMarkup = ‪$useMarkup;
74 
75  $fileName = bin2hex(random_bytes(4));
76  $folderName = bin2hex(random_bytes(4));
77  $this->assetLocation = new ‪FileLocation(sprintf('/typo3temp/assets/%s.tmp/', $folderName));
78  $fileadminDir = rtrim(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] ?? 'fileadmin', '/');
79  $this->fileadminLocation = new ‪FileLocation(sprintf('/%s/%s.tmp/', $fileadminDir, $folderName));
80  $this->fileDeclarations = $this->‪initializeFileDeclarations($fileName);
81  }
82 
83  public function ‪asStatus(): ‪Status
84  {
85  ‪$messageQueue = $this->‪getStatus();
86  $messages = [];
87  foreach (‪$messageQueue->‪getAllMessages() as $flashMessage) {
88  $messages[] = $flashMessage->getMessage();
89  }
90  $detailsLink = sprintf(
91  '<p><a href="%s" rel="noreferrer" target="_blank">%s</a></p>',
92  'https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/9.5.x/Feature-91354-IntegrateServerResponseSecurityChecks.html',
93  'Please see documentation for further details...'
94  );
96  $title = 'Potential vulnerabilities';
97  $label = $detailsLink;
98  $severity = ‪Status::ERROR;
100  $title = 'Warnings';
101  $label = $detailsLink;
102  $severity = ‪Status::WARNING;
103  }
104  return new Status(
105  'Server Response',
106  $title ?? 'OK',
107  $this->‪wrapList($messages, $label ?? '', self::WRAP_NESTED),
108  $severity ?? ‪Status::OK
109  );
110  }
111 
112  public function ‪getStatus(): ‪FlashMessageQueue
113  {
114  ‪$messageQueue = new ‪FlashMessageQueue('install-server-response-check');
115  if (PHP_SAPI === 'cli-server') {
117  new ‪FlashMessage(
118  'Skipped for PHP_SAPI=cli-server',
119  'Checks skipped',
121  )
122  );
123  return ‪$messageQueue;
124  }
125  try {
126  $this->‪buildFileDeclarations();
130  } finally {
132  }
133  return ‪$messageQueue;
134  }
135 
136  protected function ‪initializeFileDeclarations(string $fileName): array
137  {
138  $cspClosure = function (ResponseInterface $response): ?StatusMessage {
139  $cspHeader = new ContentSecurityPolicyHeader(
140  $response->getHeaderLine('content-security-policy')
141  );
142 
143  if ($cspHeader->isEmpty()) {
144  return new StatusMessage(
145  'missing Content-Security-Policy for this location'
146  );
147  }
148  if (!$cspHeader->mitigatesCrossSiteScripting()) {
149  return new StatusMessage(
150  'weak Content-Security-Policy for this location "%s"',
151  $response->getHeaderLine('content-security-policy')
152  );
153  }
154  return null;
155  };
156 
157  return [
158  (new FileDeclaration($this->assetLocation, $fileName . '.html'))
159  ->withExpectedContentType('text/html')
160  ->withExpectedContent('HTML content'),
161  (new FileDeclaration($this->assetLocation, $fileName . '.wrong'))
162  ->withUnexpectedContentType('text/html')
163  ->withExpectedContent('HTML content'),
164  (new FileDeclaration($this->assetLocation, $fileName . '.html.wrong'))
165  ->withUnexpectedContentType('text/html')
166  ->withExpectedContent('HTML content'),
167  (new FileDeclaration($this->assetLocation, $fileName . '.1.svg.wrong'))
169  ->withUnexpectedContentType('image/svg+xml')
170  ->withExpectedContent('SVG content'),
171  (new FileDeclaration($this->assetLocation, $fileName . '.2.svg.wrong'))
173  ->withUnexpectedContentType('image/svg')
174  ->withExpectedContent('SVG content'),
175  (new FileDeclaration($this->assetLocation, $fileName . '.php.wrong', true))
177  ->withUnexpectedContent('PHP content'),
178  (new FileDeclaration($this->assetLocation, $fileName . '.html.txt'))
179  ->withExpectedContentType('text/plain')
180  ->withUnexpectedContentType('text/html')
181  ->withExpectedContent('HTML content'),
182  (new FileDeclaration($this->assetLocation, $fileName . '.php.txt', true))
184  ->withUnexpectedContent('PHP content'),
185  (new FileDeclaration($this->fileadminLocation, $fileName . '.html'))
187  ->withHandler($cspClosure),
188  (new FileDeclaration($this->fileadminLocation, $fileName . '.svg'))
190  ->withHandler($cspClosure),
191  ];
192  }
193 
194  protected function ‪buildFileDeclarations(): void
195  {
196  foreach ($this->fileDeclarations as $fileDeclaration) {
197  $filePath = $fileDeclaration->getFileLocation()->getFilePath();
198  if (!is_dir($filePath)) {
200  }
201  file_put_contents(
202  $filePath . $fileDeclaration->getFileName(),
203  $fileDeclaration->buildContent()
204  );
205  }
206  }
207 
208  protected function ‪purgeFileDeclarations(): void
209  {
210  ‪GeneralUtility::rmdir($this->assetLocation->getFilePath(), true);
211  ‪GeneralUtility::rmdir($this->fileadminLocation->getFilePath(), true);
212  }
213 
215  {
216  $random = GeneralUtility::makeInstance(Random::class);
217  $randomHost = $random->generateRandomHexString(10) . '.random.example.org';
218  $time = (string)time();
219  $url = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
220  'install.server-response-check.host',
221  ['src-time' => $time, 'src-hash' => ‪ServerResponseCheckController::hmac($time)],
223  );
224  try {
225  $client = new Client(['timeout' => 10]);
226  $response = $client->request('GET', (string)$url, [
227  'headers' => ['Host' => $randomHost],
228  'allow_redirects' => false,
229  'verify' => false,
230  ]);
231  } catch (TransferException $exception) {
232  // it is expected that the previous request fails
233  return;
234  }
235  // in case we end up here, the server processed an HTTP request with invalid HTTP host header
236  $messageParts = [];
237  $locationHeader = $response->getHeaderLine('location');
238  if (!empty($locationHeader) && (new Uri($locationHeader))->getHost() === $randomHost) {
239  $messageParts[] = sprintf('HTTP Location header contained unexpected "%s"', $randomHost);
240  }
241  $data = json_decode((string)$response->getBody(), true);
242  $serverHttpHost = $data['server.HTTP_HOST'] ?? null;
243  $serverServerName = $data['server.SERVER_NAME'] ?? null;
244  if ($serverHttpHost === $randomHost) {
245  $messageParts[] = sprintf('HTTP_HOST contained unexpected "%s"', $randomHost);
246  }
247  if ($serverServerName === $randomHost) {
248  $messageParts[] = sprintf('SERVER_NAME contained unexpected "%s"', $randomHost);
249  }
250  if ($messageParts !== []) {
252  new FlashMessage(
253  $this->‪wrapList($messageParts, (string)$url, self::WRAP_FLAT),
254  'Unexpected server response',
256  )
257  );
258  }
259  }
260 
262  {
263  $promises = [];
264  $client = new Client(['timeout' => 10]);
265  foreach ($this->fileDeclarations as $fileDeclaration) {
266  $promises[] = $client->requestAsync('GET', $fileDeclaration->getUrl());
267  }
268  foreach (settle($promises)->wait() as $index => $response) {
269  $fileDeclaration = $this->fileDeclarations[$index];
270  if (($response['reason'] ?? null) instanceof BadResponseException) {
272  new FlashMessage(
273  sprintf(
274  '(%d): %s',
275  $response['reason']->getCode(),
276  $response['reason']->getRequest()->getUri()
277  ),
278  'HTTP warning',
280  )
281  );
282  continue;
283  }
284  if (!($response['value'] ?? null) instanceof ResponseInterface || $fileDeclaration->matches($response['value'])) {
285  continue;
286  }
288  new FlashMessage(
289  $this->‪createMismatchMessage($fileDeclaration, $response['value']),
290  'Unexpected server response',
291  $fileDeclaration->shallFail() ? ‪FlashMessage::ERROR : ‪FlashMessage::WARNING
292  )
293  );
294  }
295  }
296 
298  {
301  return;
302  }
304  new FlashMessage(
305  sprintf('All %d files processed correctly', count($this->fileDeclarations)),
306  'Expected server response',
308  )
309  );
310  }
311 
312  protected function ‪createMismatchMessage(‪FileDeclaration $fileDeclaration, ResponseInterface $response): string
313  {
314  $messageParts = array_map(
315  function (‪StatusMessage $mismatch): string {
316  return vsprintf(
317  $mismatch->‪getMessage(),
318  $this->wrapValues($mismatch->‪getValues(), '<code>', '</code>')
319  );
320  },
321  $fileDeclaration->‪getMismatches($response)
322  );
323  return $this->‪wrapList($messageParts, $fileDeclaration->‪getUrl(), self::WRAP_FLAT);
324  }
325 
326  protected function ‪wrapList(array $items, string $label, int $style): string
327  {
328  if (!$this->useMarkup) {
329  return sprintf(
330  '%s%s',
331  $label ? $label . ': ' : '',
332  implode(', ', $items)
333  );
334  }
335  if ($style === self::WRAP_NESTED) {
336  return sprintf(
337  '%s<ul>%s</ul>',
338  $label,
339  implode('', $this->‪wrapItems($items, '<li>', '</li>'))
340  );
341  }
342  return sprintf(
343  '<p>%s%s</p>',
344  $label,
345  implode('', $this->‪wrapItems($items, '<br>', ''))
346  );
347  }
348 
349  protected function ‪wrapItems(array $items, string $before, string $after): array
350  {
351  return array_map(
352  function (string $item) use ($before, $after): string {
353  return $before . $item . $after;
354  },
355  array_filter($items)
356  );
357  }
358 
359  protected function ‪wrapValues(array $values, string $before, string $after): array
360  {
361  return array_map(
362  function (string $value) use ($before, $after): string {
363  return $this->‪wrapValue($value, $before, $after);
364  },
365  array_filter($values)
366  );
367  }
368 
369  protected function ‪wrapValue(string $value, string $before, string $after): string
370  {
371  if ($this->useMarkup) {
372  return $before . htmlspecialchars($value) . $after;
373  }
374  return $value;
375  }
376 }
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\$messageQueue
‪FlashMessageQueue $messageQueue
Definition: ServerResponseCheck.php:52
‪TYPO3\CMS\Install\Controller\ServerResponseCheckController\hmac
‪static hmac(string $value)
Definition: ServerResponseCheckController.php:31
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\$fileadminLocation
‪FileLocation $fileadminLocation
Definition: ServerResponseCheck.php:60
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\asStatus
‪asStatus()
Definition: ServerResponseCheck.php:78
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileDeclaration\FLAG_BUILD_SVG
‪const FLAG_BUILD_SVG
Definition: FileDeclaration.php:31
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\processFileDeclarations
‪processFileDeclarations(FlashMessageQueue $messageQueue)
Definition: ServerResponseCheck.php:256
‪TYPO3\CMS\Install\SystemEnvironment\CheckInterface
Definition: CheckInterface.php:31
‪TYPO3\CMS\Reports\Status\ERROR
‪const ERROR
Definition: Status.php:29
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\StatusMessage
Definition: StatusMessage.php:24
‪TYPO3\CMS\Install\Controller\ServerResponseCheckController
Definition: ServerResponseCheckController.php:30
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\buildFileDeclarations
‪buildFileDeclarations()
Definition: ServerResponseCheck.php:189
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\getStatus
‪getStatus()
Definition: ServerResponseCheck.php:107
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileDeclaration\getMismatches
‪StatusMessage[] getMismatches(ResponseInterface $response)
Definition: FileDeclaration.php:112
‪TYPO3\CMS\Core\Messaging\FlashMessageQueue\addMessage
‪addMessage(FlashMessage $message)
Definition: FlashMessageQueue.php:79
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\WRAP_FLAT
‪const WRAP_FLAT
Definition: ServerResponseCheck.php:43
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:29
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\processHostCheck
‪processHostCheck(FlashMessageQueue $messageQueue)
Definition: ServerResponseCheck.php:209
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\purgeFileDeclarations
‪purgeFileDeclarations()
Definition: ServerResponseCheck.php:203
‪TYPO3\CMS\Reports\Status\OK
‪const OK
Definition: Status.php:27
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ContentSecurityPolicyHeader
Definition: ContentSecurityPolicyHeader.php:26
‪TYPO3\CMS\Reports\Status
Definition: Status.php:24
‪TYPO3\CMS\Core\Messaging\AbstractMessage\WARNING
‪const WARNING
Definition: AbstractMessage.php:30
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse
Definition: ContentSecurityPolicyDirective.php:18
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileDeclaration\getUrl
‪string getUrl()
Definition: FileDeclaration.php:219
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep($directory)
Definition: GeneralUtility.php:2022
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileLocation
Definition: FileLocation.php:30
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:38
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\initializeFileDeclarations
‪initializeFileDeclarations(string $fileName)
Definition: ServerResponseCheck.php:131
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\StatusMessage\getValues
‪string[] getValues()
Definition: StatusMessage.php:50
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileDeclaration\FLAG_BUILD_SVG_DOCUMENT
‪const FLAG_BUILD_SVG_DOCUMENT
Definition: FileDeclaration.php:33
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck
Definition: ServerResponseCheck.php:42
‪TYPO3\CMS\Core\Messaging\FlashMessageQueue\getAllMessages
‪FlashMessage[] getAllMessages($severity=null)
Definition: FlashMessageQueue.php:111
‪TYPO3\CMS\Reports\Status\WARNING
‪const WARNING
Definition: Status.php:28
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\$assetLocation
‪FileLocation $assetLocation
Definition: ServerResponseCheck.php:56
‪TYPO3\CMS\Core\Messaging\AbstractMessage\OK
‪const OK
Definition: AbstractMessage.php:29
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileDeclaration\FLAG_BUILD_PHP
‪const FLAG_BUILD_PHP
Definition: FileDeclaration.php:30
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\WRAP_NESTED
‪const WRAP_NESTED
Definition: ServerResponseCheck.php:44
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:24
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\wrapItems
‪wrapItems(array $items, string $before, string $after)
Definition: ServerResponseCheck.php:344
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileDeclaration
Definition: FileDeclaration.php:28
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\createMismatchMessage
‪createMismatchMessage(FileDeclaration $fileDeclaration, ResponseInterface $response)
Definition: ServerResponseCheck.php:307
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\StatusMessage\getMessage
‪string getMessage()
Definition: StatusMessage.php:42
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\__construct
‪__construct(bool $useMarkup=true)
Definition: ServerResponseCheck.php:66
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\finishMessageQueue
‪finishMessageQueue(FlashMessageQueue $messageQueue)
Definition: ServerResponseCheck.php:292
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\$fileDeclarations
‪FileDeclaration[] $fileDeclarations
Definition: ServerResponseCheck.php:64
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\$useMarkup
‪bool $useMarkup
Definition: ServerResponseCheck.php:48
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir($path, $removeNonEmpty=false)
Definition: GeneralUtility.php:2075
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\wrapList
‪wrapList(array $items, string $label, int $style)
Definition: ServerResponseCheck.php:321
‪TYPO3\CMS\Core\Crypto\Random
Definition: Random.php:24
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\FileDeclaration\FLAG_BUILD_HTML_DOCUMENT
‪const FLAG_BUILD_HTML_DOCUMENT
Definition: FileDeclaration.php:32
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Backend\Routing\UriBuilder\ABSOLUTE_URL
‪const ABSOLUTE_URL
Definition: UriBuilder.php:42
‪TYPO3\CMS\Core\Messaging\FlashMessageQueue
Definition: FlashMessageQueue.php:29
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\wrapValues
‪wrapValues(array $values, string $before, string $after)
Definition: ServerResponseCheck.php:354
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\wrapValue
‪wrapValue(string $value, string $before, string $after)
Definition: ServerResponseCheck.php:364
‪TYPO3\CMS\Core\Messaging\AbstractMessage\ERROR
‪const ERROR
Definition: AbstractMessage.php:31