3 declare(strict_types = 1);
20 use GuzzleHttp\Client;
21 use GuzzleHttp\Exception\BadResponseException;
22 use
function GuzzleHttp\Promise\settle;
23 use Psr\Http\Message\ResponseInterface;
70 $fileName = bin2hex(random_bytes(4));
71 $folderName = bin2hex(random_bytes(4));
72 $this->assetLocation =
new FileLocation(sprintf(
'/typo3temp/assets/%s.tmp/', $folderName));
73 $fileadminDir = rtrim(
$GLOBALS[
'TYPO3_CONF_VARS'][
'BE'][
'fileadminDir'] ??
'fileadmin',
'/');
74 $this->fileadminLocation =
new FileLocation(sprintf(
'/%s/%s.tmp/', $fileadminDir, $folderName));
83 $messages[] = $flashMessage->getMessage();
85 $detailsLink = sprintf(
86 '<p><a href="%s" rel="noreferrer" target="_blank">%s</a></p>',
87 'https://docs.typo3.org/c/typo3/cms-core/master/en-us/Changelog/9.5.x/Feature-91354-IntegrateServerResponseSecurityChecks.html',
88 'Please see documentation for further details...'
91 $title =
'Potential vulnerabilities';
92 $label = $detailsLink;
96 $label = $detailsLink;
100 'Server Response on static files',
102 $this->
wrapList($messages, $label ??
'', self::WRAP_NESTED),
110 if (PHP_SAPI ===
'cli-server') {
113 'Skipped for PHP_SAPI=cli-server',
132 $cspClosure =
function (ResponseInterface $response): ?StatusMessage {
133 $cspHeader =
new ContentSecurityPolicyHeader(
134 $response->getHeaderLine(
'content-security-policy')
137 if ($cspHeader->isEmpty()) {
138 return new StatusMessage(
139 'missing Content-Security-Policy for this location'
142 if (!$cspHeader->mitigatesCrossSiteScripting()) {
143 return new StatusMessage(
144 'weak Content-Security-Policy for this location "%s"',
145 $response->getHeaderLine(
'content-security-policy')
152 (
new FileDeclaration($this->assetLocation, $fileName .
'.html'))
153 ->withExpectedContentType(
'text/html')
154 ->withExpectedContent(
'HTML content'),
155 (
new FileDeclaration($this->assetLocation, $fileName .
'.wrong'))
156 ->withUnexpectedContentType(
'text/html')
157 ->withExpectedContent(
'HTML content'),
158 (
new FileDeclaration($this->assetLocation, $fileName .
'.html.wrong'))
159 ->withUnexpectedContentType(
'text/html')
160 ->withExpectedContent(
'HTML content'),
161 (
new FileDeclaration($this->assetLocation, $fileName .
'.1.svg.wrong'))
163 ->withUnexpectedContentType(
'image/svg+xml')
164 ->withExpectedContent(
'SVG content'),
165 (
new FileDeclaration($this->assetLocation, $fileName .
'.2.svg.wrong'))
167 ->withUnexpectedContentType(
'image/svg')
168 ->withExpectedContent(
'SVG content'),
169 (
new FileDeclaration($this->assetLocation, $fileName .
'.php.wrong',
true))
171 ->withUnexpectedContent(
'PHP content'),
172 (
new FileDeclaration($this->assetLocation, $fileName .
'.html.txt'))
173 ->withExpectedContentType(
'text/plain')
174 ->withUnexpectedContentType(
'text/html')
175 ->withExpectedContent(
'HTML content'),
176 (
new FileDeclaration($this->assetLocation, $fileName .
'.php.txt',
true))
178 ->withUnexpectedContent(
'PHP content'),
179 (
new FileDeclaration($this->fileadminLocation, $fileName .
'.html'))
181 ->withHandler($cspClosure),
182 (
new FileDeclaration($this->fileadminLocation, $fileName .
'.svg'))
184 ->withHandler($cspClosure),
190 foreach ($this->fileDeclarations as $fileDeclaration) {
191 $filePath = $fileDeclaration->getFileLocation()->getFilePath();
192 if (!is_dir($filePath)) {
193 GeneralUtility::mkdir_deep($filePath);
196 $filePath . $fileDeclaration->getFileName(),
197 $fileDeclaration->buildContent()
204 GeneralUtility::rmdir($this->assetLocation->getFilePath(),
true);
205 GeneralUtility::rmdir($this->fileadminLocation->getFilePath(),
true);
211 $client =
new Client([
'timeout' => 10]);
212 foreach ($this->fileDeclarations as $fileDeclaration) {
213 $promises[] = $client->requestAsync(
'GET', $fileDeclaration->getUrl());
215 foreach (settle($promises)->wait() as $index => $response) {
216 $fileDeclaration = $this->fileDeclarations[$index];
217 if (($response[
'reason'] ??
null) instanceof BadResponseException) {
222 $response[
'reason']->getCode(),
223 $response[
'reason']->getRequest()->getUri()
231 if (!($response[
'value'] ??
null) instanceof ResponseInterface || $fileDeclaration->matches($response[
'value'])) {
237 'Unexpected server response',
252 sprintf(
'All %d files processed correctly', count($this->fileDeclarations)),
253 'Expected server response',
261 $messageParts = array_map(
265 $this->wrapValues($mismatch->
getValues(),
'<code>',
'</code>')
270 return $this->
wrapList($messageParts, $fileDeclaration->
getUrl(), self::WRAP_FLAT);
273 protected function wrapList(array $items,
string $label,
int $style): string
275 if (!$this->useMarkup) {
278 $label ? $label .
': ' :
'',
279 implode(
', ', $items)
282 if ($style === self::WRAP_NESTED) {
286 implode(
'', $this->
wrapItems($items,
'<li>',
'</li>'))
292 implode(
'', $this->
wrapItems($items,
'<br>',
''))
296 protected function wrapItems(array $items,
string $before,
string $after): array
299 function (
string $item) use ($before, $after):
string {
300 return $before . $item . $after;
306 protected function wrapValues(array $values,
string $before,
string $after): array
309 function (
string $value) use ($before, $after):
string {
310 return $this->
wrapValue($value, $before, $after);
312 array_filter($values)
316 protected function wrapValue(
string $value,
string $before,
string $after): string
318 if ($this->useMarkup) {
319 return $before . htmlspecialchars($value) . $after;