TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Http\Client;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use Psr\Http\Message\RequestInterface;
/**
* Guzzle client middleware for filtering allowed request targets (SSRF prevention)
*
* @internal
*/
final readonly class AllowedHostsMiddleware
{
public function __construct(
private string $context,
private array $allowedHosts,
) {}
/**
* @param callable(RequestInterface, array): PromiseInterface $nextHandler
* @return callable(RequestInterface $request, array $options): PromiseInterface
*/
public function __invoke(callable $nextHandler): callable
{
return fn(RequestInterface $request, array $options): PromiseInterface
=> $this->matches($request->getUri()->getHost())
? $nextHandler($request, $options)
: new RejectedPromise(sprintf(
'Requested host \'%s\' is not allowed in $GLOBALS[\'TYPO3_CONF_VARS\'][\'HTTP\'][\'allowed_hosts\'][\'%s\']',
$request->getUri()->getHost(),
$this->context
));
}
private function matches(string $host): bool
{
foreach ($this->allowedHosts as $allowedHost) {
// Match wildcards
if (str_contains($allowedHost, '*')) {
$expr = implode(
'.+',
array_map(
static fn(string $part): string => preg_quote($part, '/'),
explode('*', $allowedHost)
)
);
if (preg_match('/^' . $expr . '$/', $host)) {
return true;
}
} elseif ($allowedHost === $host) {
// Exact matches
return true;
}
}
return false;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Http\Client;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
/**
* @internal
*/
readonly class GuzzleClientFactory
{
/**
* Creates the client to do requests
*/
public function getClient(?string $context = null): ClientInterface
{
$httpOptions = $GLOBALS['TYPO3_CONF_VARS']['HTTP'];
$httpOptions['verify'] = filter_var($httpOptions['verify'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $httpOptions['verify'];
// HEADS UP:
// Passing a guzzle handler stack instead of an array of middlewares has never been documented,
// but was theoretically possible since the introduction of handler middlewares.
// This is not considered API (we can not control the ordering of the AllowedHosts Middleware)
// but is preserved for maximum compatibility for now. It may vanish in a major release without notice.
$stack = ($httpOptions['handler'] ?? null) instanceof HandlerStack ? $httpOptions['handler'] : HandlerStack::create();
if ($context !== null) {
$allowedHosts = $httpOptions['allowed_hosts'][$context] ?? null;
if (is_array($allowedHosts)) {
$stack->push(
new AllowedHostsMiddleware($context, array_filter($allowedHosts, is_string(...))),
'typo3_allowed_hosts'
);
}
}
unset($httpOptions['allowed_hosts']);
if (is_array($httpOptions['handler'] ?? null)) {
foreach ($httpOptions['handler'] as $name => $handler) {
$stack->push($handler, (string)$name);
}
}
$httpOptions['handler'] = $stack;
return new Client($httpOptions);
}
}