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