‪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  $hashService = GeneralUtility::makeInstance(HashService::class);
221  ‪$url = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
222  'install.server-response-check.host',
223  ['src-time' => $time, 'src-hash' => $hashService->hmac($time, 'server-response-check')],
225  );
226  try {
227  $client = new Client(['timeout' => 10]);
228  $response = $client->request('GET', (string)‪$url, [
229  'headers' => ['Host' => $randomHost],
230  'allow_redirects' => false,
231  'verify' => false,
232  ]);
233  } catch (TransferException $exception) {
234  // it is expected that the previous request fails
235  return;
236  }
237  // in case we end up here, the server processed an HTTP request with invalid HTTP host header
238  $messageParts = [];
239  $locationHeader = $response->getHeaderLine('location');
240  if (!empty($locationHeader) && (new Uri($locationHeader))->getHost() === $randomHost) {
241  $messageParts[] = sprintf('HTTP Location header contained unexpected "%s"', $randomHost);
242  }
243  $data = json_decode((string)$response->getBody(), true);
244  $serverHttpHost = $data['server.HTTP_HOST'] ?? null;
245  $serverServerName = $data['server.SERVER_NAME'] ?? null;
246  if ($serverHttpHost === $randomHost) {
247  $messageParts[] = sprintf('HTTP_HOST contained unexpected "%s"', $randomHost);
248  }
249  if ($serverServerName === $randomHost) {
250  $messageParts[] = sprintf('SERVER_NAME contained unexpected "%s"', $randomHost);
251  }
252  if ($messageParts !== []) {
254  new FlashMessage(
255  $this->‪wrapList($messageParts, (string)‪$url, self::WRAP_FLAT),
256  'Unexpected server response',
257  ContextualFeedbackSeverity::ERROR
258  )
259  );
260  }
261  }
262 
264  {
265  $promises = [];
266  $client = new Client(['timeout' => 10]);
267  foreach ($this->fileDeclarations as $fileDeclaration) {
268  $promises[] = $client->requestAsync('GET', $fileDeclaration->getUrl());
269  }
270  foreach (Utils::settle($promises)->wait() as $index => $response) {
271  $fileDeclaration = $this->fileDeclarations[$index];
272  if (($response['reason'] ?? null) instanceof BadResponseException) {
274  new FlashMessage(
275  sprintf(
276  '(%d): %s',
277  $response['reason']->getCode(),
278  $response['reason']->getRequest()->getUri()
279  ),
280  'HTTP warning',
281  ContextualFeedbackSeverity::WARNING
282  )
283  );
284  continue;
285  }
286  if (!($response['value'] ?? null) instanceof ResponseInterface || $fileDeclaration->matches($response['value'])) {
287  continue;
288  }
290  new FlashMessage(
291  $this->‪createMismatchMessage($fileDeclaration, $response['value']),
292  'Unexpected server response',
293  $fileDeclaration->shallFail() ? ContextualFeedbackSeverity::ERROR : ContextualFeedbackSeverity::WARNING
294  )
295  );
296  }
297  }
298 
300  {
301  if (‪$messageQueue->‪getAllMessages(ContextualFeedbackSeverity::WARNING) !== []
302  || ‪$messageQueue->‪getAllMessages(ContextualFeedbackSeverity::ERROR) !== []) {
303  return;
304  }
306  new FlashMessage(
307  sprintf('All %d files processed correctly', count($this->fileDeclarations)),
308  'Expected server response',
309  ContextualFeedbackSeverity::OK
310  )
311  );
312  }
313 
314  protected function ‪createMismatchMessage(‪FileDeclaration $fileDeclaration, ResponseInterface $response): string
315  {
316  $messageParts = array_map(
317  function (‪StatusMessage $mismatch): string {
318  return vsprintf(
319  $mismatch->‪getMessage(),
320  $this->wrapValues($mismatch->‪getValues(), '<code>', '</code>')
321  );
322  },
323  $fileDeclaration->‪getMismatches($response)
324  );
325  return $this->‪wrapList($messageParts, $fileDeclaration->‪getUrl(), self::WRAP_FLAT);
326  }
327 
328  protected function ‪wrapList(array $items, string $label, int $style): string
329  {
330  if (!$this->useMarkup) {
331  return sprintf(
332  '%s%s',
333  $label ? $label . ': ' : '',
334  implode(', ', $items)
335  );
336  }
337  if ($style === self::WRAP_NESTED) {
338  return sprintf(
339  '%s<ul>%s</ul>',
340  $label,
341  implode('', $this->‪wrapItems($items, '<li>', '</li>'))
342  );
343  }
344  return sprintf(
345  '<p>%s%s</p>',
346  $label,
347  implode('', $this->‪wrapItems($items, '<br>', ''))
348  );
349  }
350 
351  protected function ‪wrapItems(array $items, string $before, string $after): array
352  {
353  return array_map(
354  function (string $item) use ($before, $after): string {
355  return $before . $item . $after;
356  },
357  array_filter($items)
358  );
359  }
360 
361  protected function ‪wrapValues(array $values, string $before, string $after): array
362  {
363  return array_map(
364  function (string $value) use ($before, $after): string {
365  return $this->‪wrapValue($value, $before, $after);
366  },
367  array_filter($values)
368  );
369  }
370 
371  protected function ‪wrapValue(string $value, string $before, string $after): string
372  {
373  if ($this->useMarkup) {
374  return $before . htmlspecialchars($value) . $after;
375  }
376  return $value;
377  }
378 }
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\$messageQueue
‪FlashMessageQueue $messageQueue
Definition: ServerResponseCheck.php:53
‪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:258
‪TYPO3\CMS\Install\SystemEnvironment\CheckInterface
Definition: CheckInterface.php:31
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\StatusMessage
Definition: StatusMessage.php:24
‪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:1654
‪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:1702
‪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:346
‪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:309
‪$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:294
‪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:323
‪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:356
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck\wrapValue
‪wrapValue(string $value, string $before, string $after)
Definition: ServerResponseCheck.php:366