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