TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\Uri;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\CspConfigurationFactory;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\DispositionConfiguration;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\BeforePersistingReportEvent;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportDetails;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportRepository;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportStatus;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
|
||||
use TYPO3\CMS\Core\Utility\IpAnonymizationUtility;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractContentSecurityPolicyReporter implements MiddlewareInterface
|
||||
{
|
||||
protected const URI_KEYS = ['document-uri', 'report-uri', 'blocked-uri', 'referrer'];
|
||||
|
||||
public function __construct(
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly PolicyProvider $policyProvider,
|
||||
protected readonly CspConfigurationFactory $cspConfigurationFactory,
|
||||
protected readonly ReportRepository $reportRepository,
|
||||
protected readonly HashService $hashService,
|
||||
) {}
|
||||
|
||||
protected function persistCspReport(Scope $scope, ServerRequestInterface $request): void
|
||||
{
|
||||
$payload = (string)$request->getBody();
|
||||
if (!$this->isJson($payload)) {
|
||||
return;
|
||||
}
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$meta = [
|
||||
'addr' => IpAnonymizationUtility::anonymizeIp($normalizedParams->getRemoteAddress()),
|
||||
'agent' => $normalizedParams->getHttpUserAgent(),
|
||||
];
|
||||
// skip potential externally injected violation reports
|
||||
$requestTime = $this->getRequestQueryParam($request, 'requestTime');
|
||||
$requestHash = $this->getRequestQueryParam($request, 'requestHash');
|
||||
if ($requestTime === null
|
||||
|| $requestHash === null
|
||||
|| !$this->hashService->validateHmac($requestTime, self::class, $requestHash)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$originalDetails = json_decode($payload, true)['csp-report'] ?? [];
|
||||
$originalDetails = $this->anonymizeDetails($originalDetails);
|
||||
$details = new ReportDetails($originalDetails);
|
||||
$summary = $this->generateReportSummary($scope, $details);
|
||||
$report = new Report(
|
||||
$scope,
|
||||
ReportStatus::New,
|
||||
(int)$requestTime,
|
||||
$meta,
|
||||
$details,
|
||||
$summary
|
||||
);
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforePersistingReportEvent($report, $request)
|
||||
);
|
||||
if ($event->report !== null) {
|
||||
$this->reportRepository->add($event->report);
|
||||
}
|
||||
}
|
||||
|
||||
protected function generateReportSummary(Scope $scope, ReportDetails $details): string
|
||||
{
|
||||
return $this->hashService->hmac(
|
||||
json_encode([
|
||||
$scope,
|
||||
$details['effective-directive'] ?? null,
|
||||
$details['blocked-uri'] ?? null,
|
||||
$details['script-sample'] ?? null,
|
||||
]),
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
protected function anonymizeDetails(array $details): array
|
||||
{
|
||||
foreach (self::URI_KEYS as $uriKey) {
|
||||
if (!isset($details[$uriKey])) {
|
||||
continue;
|
||||
}
|
||||
$details[$uriKey] = $this->anonymizeUri($details[$uriKey]);
|
||||
}
|
||||
return $details;
|
||||
}
|
||||
|
||||
protected function anonymizeUri(string $value): string
|
||||
{
|
||||
try {
|
||||
$uri = Uri::fromAnyScheme($value);
|
||||
} catch (\Throwable) {
|
||||
return '';
|
||||
}
|
||||
if ($uri->getQuery() === '') {
|
||||
return $value;
|
||||
}
|
||||
parse_str($uri->getQuery(), $query);
|
||||
// strip CSRF token (might be a different usage as well)
|
||||
unset($query['token']);
|
||||
return (string)$uri->withQuery(http_build_query($query, '', '&', PHP_QUERY_RFC3986));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines, whether current request URI starts with local reporting URI,
|
||||
* (e.g. `https://ip12.anyhost.it:8443/en/@http-reporting?csp=report`).
|
||||
*/
|
||||
protected function targetsCspReportUri(Scope $scope, ServerRequestInterface $request): bool
|
||||
{
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$reportingUriBase = $this->policyProvider->getDefaultReportingUriBase($scope, $request, false);
|
||||
return str_starts_with($normalizedParams?->getRequestUri() ?? '', (string)$reportingUriBase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines, whether the request is eligible to be handled by the local reporting URI
|
||||
* (`targetsCspReportUri()` must have been called before).
|
||||
*/
|
||||
protected function isCspReport(
|
||||
Scope $scope,
|
||||
ServerRequestInterface $request,
|
||||
?DispositionConfiguration $dispositionConfiguration = null,
|
||||
): bool {
|
||||
// @todo
|
||||
// + verify current session
|
||||
// + invoke rate limiter
|
||||
// + check additional scope (snippet enrichment)
|
||||
|
||||
$reportingUrl = $this->policyProvider->getReportingUrlFor($scope, $request, $dispositionConfiguration);
|
||||
$reportingUriBase = $this->policyProvider->getDefaultReportingUriBase($scope, $request);
|
||||
$contentTypeHeader = $request->getHeaderLine('content-type');
|
||||
|
||||
return
|
||||
// stop, if reporting was explicitly disabled in `contentSecurityPolicyReportingUrl`
|
||||
$reportingUrl !== null
|
||||
// stop, if different reporting URI was configured in `contentSecurityPolicyReportingUrl`
|
||||
&& str_starts_with((string)$reportingUrl, (string)$reportingUriBase)
|
||||
// stop, if request is not `POST` or `Content-Type` is not `application/csp-report`
|
||||
&& $request->getMethod() === 'POST' && $contentTypeHeader === 'application/csp-report';
|
||||
}
|
||||
|
||||
protected function getRequestQueryParam(ServerRequestInterface $request, string $name): ?string
|
||||
{
|
||||
$value = $request->getQueryParams()[$name] ?? null;
|
||||
return is_string($value) ? $value : null;
|
||||
}
|
||||
|
||||
protected function isJson(string $value): bool
|
||||
{
|
||||
try {
|
||||
json_decode($value, false, 16, JSON_THROW_ON_ERROR);
|
||||
return true;
|
||||
} catch (\JsonException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\UserAspect;
|
||||
use TYPO3\CMS\Core\Context\WorkspaceAspect;
|
||||
|
||||
/**
|
||||
* Boilerplate to authenticate a backend user in the current workflow, can be used
|
||||
* for TYPO3 Backend and Frontend requests.
|
||||
*
|
||||
* The actual authentication and the selection if no-cache headers to responses should
|
||||
* be applied should still reside in the "process()" method which should be
|
||||
* extended by derivative classes.
|
||||
*
|
||||
* In derivative classes, the Context API can be used to detect, if a backend user is logged in
|
||||
* like this:
|
||||
*
|
||||
* ```
|
||||
* $response = $handler->handle($request);
|
||||
* if ($this->context->getAspect('backend.user')->isLoggedIn()) {
|
||||
* return $this->applyHeadersToResponse($response);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @internal this class might get merged again with the subclasses
|
||||
*/
|
||||
abstract class BackendUserAuthenticator implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(protected Context $context) {}
|
||||
|
||||
abstract public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
|
||||
|
||||
/**
|
||||
* Adding headers to the response to avoid caching on the client side.
|
||||
* These headers will override any previous headers of these names sent.
|
||||
* Get the http headers to be sent if an authenticated user is available,
|
||||
* in order to disallow browsers to store the response on the client side.
|
||||
*
|
||||
* @return ResponseInterface the modified response object.
|
||||
*/
|
||||
protected function applyHeadersToResponse(ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
if ($response->getHeaderLine('Cache-Control')) {
|
||||
return $response;
|
||||
}
|
||||
$headers = [
|
||||
'Expires' => 0,
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
|
||||
'Cache-Control' => 'no-cache, no-store',
|
||||
// HTTP 1.0 compatibility, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma
|
||||
'Pragma' => 'no-cache',
|
||||
];
|
||||
foreach ($headers as $headerName => $headerValue) {
|
||||
$response = $response->withHeader($headerName, (string)$headerValue);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the backend user as aspect
|
||||
*/
|
||||
protected function setBackendUserAspect(?BackendUserAuthentication $user, ?int $alternativeWorkspaceId = null): void
|
||||
{
|
||||
$this->context->setAspect('backend.user', new UserAspect($user));
|
||||
$this->context->setAspect('workspace', new WorkspaceAspect($alternativeWorkspaceId ?? $user->workspace ?? 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Cache\CacheDataCollector;
|
||||
use TYPO3\CMS\Core\Cache\CacheTag;
|
||||
use TYPO3\CMS\Core\Cache\Event\AddCacheTagEvent;
|
||||
|
||||
/**
|
||||
* Add CacheTags as 'cacheDataCollector' attribute.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CacheDataCollectorAttribute implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* The maximum length of the X-TYPO3-Cache-Tags header.
|
||||
* Prevents exceeding the maximum header size of 8kB.
|
||||
* Some web servers (e.g. nginx or apache2) have a default
|
||||
* limit of 8kB for the size of a single header.
|
||||
*/
|
||||
private const int MAX_CACHE_TAGS_HEADER_LENGTH = 8000;
|
||||
|
||||
private ?CacheDataCollector $cacheDataCollector = null;
|
||||
|
||||
/**
|
||||
* Adds an instance of TYPO3\CMS\Core\Cache\CacheDataCollector as
|
||||
* attribute to $request object
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// Middlewares are shared services. With sub requests in mind, we need to take care
|
||||
// existing data of the parent request is not overwritten by a sub request. We thus
|
||||
// back up any existing data, create a new CacheDataCollector, dispatch the request,
|
||||
// and reset before returning the response.
|
||||
// @todo: We could argue sub requests should run their own container instance, but
|
||||
// this has more impact and has not been sorted out, yet.
|
||||
$backup = $this->cacheDataCollector;
|
||||
$this->cacheDataCollector = new CacheDataCollector();
|
||||
$request = $request->withAttribute('frontend.cache.collector', $this->cacheDataCollector);
|
||||
$response = $handler->handle($request);
|
||||
if ($this->isDebugModeEnabled()) {
|
||||
$cacheTags = array_map(fn(CacheTag $cacheTag) => $cacheTag->name, $this->cacheDataCollector->getCacheTags());
|
||||
sort($cacheTags);
|
||||
foreach (explode("\n", wordwrap(implode(' ', $cacheTags), self::MAX_CACHE_TAGS_HEADER_LENGTH, "\n")) as $delta => $tags) {
|
||||
$response = $response->withHeader('X-TYPO3-Cache-Tags' . ($delta > 0 ? '-' . $delta : ''), $tags);
|
||||
}
|
||||
$response = $response->withHeader('X-TYPO3-Cache-Lifetime', (string)$this->cacheDataCollector->resolveLifetime());
|
||||
}
|
||||
foreach ($this->cacheDataCollector->getCacheEntries() as $deferredCacheItem) {
|
||||
$deferredCacheItem($request);
|
||||
}
|
||||
$this->cacheDataCollector = $backup;
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[AsEventListener]
|
||||
public function onCacheTagAdded(AddCacheTagEvent $event): void
|
||||
{
|
||||
// This event listener is a tribute to code places that want to add cache tags,
|
||||
// but don't have the request available to set cache tags on the attribute directly.
|
||||
// TYPO3 core will try to get rid of these places over time, but needs this event
|
||||
// listener for now.
|
||||
$this->cacheDataCollector?->addCacheTags($event->cacheTag);
|
||||
}
|
||||
|
||||
private function isDebugModeEnabled(): bool
|
||||
{
|
||||
return !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
|
||||
/**
|
||||
* Add NormalizedParams as 'normalizedParams' attribute.
|
||||
* Used in FE, BE and install tool context.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class NormalizedParamsAttribute implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Adds an instance of TYPO3\CMS\Core\Http\NormalizedParams as
|
||||
* attribute to $request object
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$normalizedParams = $request->getAttribute('normalizedParams', null);
|
||||
if ($normalizedParams === null) {
|
||||
$request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request));
|
||||
}
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\SecurityAspect;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Security\Nonce;
|
||||
use TYPO3\CMS\Core\Security\NonceException;
|
||||
use TYPO3\CMS\Core\Security\NoncePool;
|
||||
use TYPO3\CMS\Core\Security\RequestToken;
|
||||
use TYPO3\CMS\Core\Security\RequestTokenException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RequestTokenMiddleware implements MiddlewareInterface
|
||||
{
|
||||
protected const COOKIE_PREFIX = 'typo3nonce_';
|
||||
protected const SECURE_PREFIX = '__Secure-';
|
||||
|
||||
protected const ALLOWED_METHODS = ['POST', 'PUT', 'PATCH'];
|
||||
|
||||
protected SecurityAspect $securityAspect;
|
||||
protected NoncePool $noncePool;
|
||||
|
||||
public function __construct(
|
||||
Context $context,
|
||||
protected readonly LoggerInterface $logger,
|
||||
) {
|
||||
$this->securityAspect = SecurityAspect::provideIn($context);
|
||||
$this->noncePool = $this->securityAspect->getNoncePool();
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// @todo some™ route handling mechanism might verify request-tokens (-> e.g. backend-routes, unsure for frontend)
|
||||
$this->noncePool->merge($this->resolveNoncePool($request))->purge();
|
||||
|
||||
try {
|
||||
$this->securityAspect->setReceivedRequestToken($this->resolveReceivedRequestToken($request));
|
||||
} catch (RequestTokenException $exception) {
|
||||
// request token was given, but could not be verified
|
||||
$this->securityAspect->setReceivedRequestToken(false);
|
||||
$this->logger->debug('Could not resolve request token', ['exception' => $exception]);
|
||||
}
|
||||
|
||||
$response = $handler->handle($request);
|
||||
return $this->enrichResponseWithCookie($request, $response);
|
||||
}
|
||||
|
||||
protected function resolveNoncePool(ServerRequestInterface $request): NoncePool
|
||||
{
|
||||
$secure = $this->isHttps($request);
|
||||
// resolves cookie name dependent on whether TLS is used in request and uses `__Secure-` prefix,
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes
|
||||
$securePrefix = $secure ? self::SECURE_PREFIX : '';
|
||||
$cookiePrefix = $securePrefix . self::COOKIE_PREFIX;
|
||||
$cookiePrefixLength = strlen($cookiePrefix);
|
||||
$cookies = array_filter(
|
||||
$request->getCookieParams(),
|
||||
static fn(mixed $name): bool => is_string($name) && str_starts_with($name, $cookiePrefix),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
$items = [];
|
||||
foreach ($cookies as $name => $value) {
|
||||
$name = substr($name, $cookiePrefixLength);
|
||||
try {
|
||||
$items[$name] = Nonce::fromHashSignedJwt($value);
|
||||
} catch (NonceException $exception) {
|
||||
$this->logger->debug('Could not resolve received nonce', ['exception' => $exception]);
|
||||
$items[$name] = null;
|
||||
}
|
||||
}
|
||||
// @todo pool `$options` should be configurable via `$TYPO3_CONF_VARS`
|
||||
return GeneralUtility::makeInstance(NoncePool::class, $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RequestTokenException
|
||||
*/
|
||||
protected function resolveReceivedRequestToken(ServerRequestInterface $request): ?RequestToken
|
||||
{
|
||||
$headerValue = $request->getHeaderLine(RequestToken::HEADER_NAME);
|
||||
$paramValue = '';
|
||||
if (isset($request->getParsedBody()[RequestToken::PARAM_NAME]) && is_scalar($request->getParsedBody()[RequestToken::PARAM_NAME])) {
|
||||
$paramValue = (string)($request->getParsedBody()[RequestToken::PARAM_NAME]);
|
||||
}
|
||||
if ($headerValue !== '') {
|
||||
$tokenValue = $headerValue;
|
||||
} elseif (in_array($request->getMethod(), self::ALLOWED_METHODS, true)) {
|
||||
$tokenValue = $paramValue;
|
||||
} else {
|
||||
$tokenValue = '';
|
||||
}
|
||||
if ($tokenValue === '') {
|
||||
return null;
|
||||
}
|
||||
return RequestToken::fromHashSignedJwt($tokenValue, $this->securityAspect->getSigningSecretResolver());
|
||||
}
|
||||
|
||||
protected function enrichResponseWithCookie(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$secure = $this->isHttps($request);
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$path = $normalizedParams->getSitePath();
|
||||
$securePrefix = $secure ? self::SECURE_PREFIX : '';
|
||||
$cookiePrefix = $securePrefix . self::COOKIE_PREFIX;
|
||||
|
||||
$createCookie = static fn(string $name, string $value, int $expire): Cookie => new Cookie(
|
||||
$name,
|
||||
$value,
|
||||
$expire,
|
||||
$path,
|
||||
null,
|
||||
$secure,
|
||||
true,
|
||||
false,
|
||||
Cookie::SAMESITE_STRICT
|
||||
);
|
||||
|
||||
$cookies = [];
|
||||
// emit new nonce cookies
|
||||
foreach ($this->noncePool->getEmittableNonces() as $name => $nonce) {
|
||||
$cookies[] = $createCookie($cookiePrefix . $name, $nonce->toHashSignedJwt(), 0);
|
||||
}
|
||||
// revoke nonce cookies (exceeded pool size, expired or explicitly revoked)
|
||||
foreach ($this->noncePool->getRevocableNames() as $name) {
|
||||
$cookies[] = $createCookie($cookiePrefix . $name, '', -1);
|
||||
}
|
||||
// finally apply to response
|
||||
foreach ($cookies as $cookie) {
|
||||
$response = $response->withAddedHeader('Set-Cookie', (string)$cookie);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function isHttps(ServerRequestInterface $request): bool
|
||||
{
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
return $normalizedParams instanceof NormalizedParams && $normalizedParams->isHttps();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly class ResponsePropagation implements MiddlewareInterface
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$response = $handler->handle($request);
|
||||
} catch (PropagateResponseException $e) {
|
||||
$response = $e->getResponse();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?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\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Checks if the provided host header value matches the trusted hosts pattern.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class VerifyHostHeader implements MiddlewareInterface
|
||||
{
|
||||
public const ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL = '.*';
|
||||
public const ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME = 'SERVER_NAME';
|
||||
|
||||
protected string $trustedHostsPattern;
|
||||
|
||||
public function __construct(string $trustedHostsPattern)
|
||||
{
|
||||
$this->trustedHostsPattern = $trustedHostsPattern;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$serverParams = $request->getServerParams();
|
||||
$httpHost = $serverParams['HTTP_HOST'] ?? '';
|
||||
if (!$this->isAllowedHostHeaderValue($httpHost, $serverParams)) {
|
||||
throw new \UnexpectedValueException(
|
||||
'The current host header value does not match the configured trusted hosts pattern!'
|
||||
. ' Check the pattern defined in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'trustedHostsPattern\']'
|
||||
. ' and adapt it, if you want to allow the current host header \'' . $httpHost . '\' for your installation.',
|
||||
1396795884
|
||||
);
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided host header value matches the trusted hosts pattern.
|
||||
*
|
||||
* @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
|
||||
*/
|
||||
public function isAllowedHostHeaderValue(string $hostHeaderValue, array $serverParams): bool
|
||||
{
|
||||
// Deny the value if trusted host patterns is empty, which means configuration is invalid.
|
||||
if ($this->trustedHostsPattern === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->trustedHostsPattern === self::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue, $serverParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided host header value matches the trusted hosts pattern without any preprocessing.
|
||||
*/
|
||||
protected function hostHeaderValueMatchesTrustedHostsPattern(string $hostHeaderValue, array $serverParams): bool
|
||||
{
|
||||
if ($this->trustedHostsPattern === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
|
||||
$host = strtolower($hostHeaderValue);
|
||||
// Default port to be verified if HTTP_HOST does not contain explicit port information.
|
||||
// Deriving from raw/local webserver HTTPS information (not taking possible proxy configurations into account)
|
||||
// as we compare against the raw/local server information (SERVER_PORT).
|
||||
$port = self::webserverUsesHttps($serverParams) ? '443' : '80';
|
||||
|
||||
$parsedHostValue = parse_url('http://' . $host);
|
||||
if (isset($parsedHostValue['port'])) {
|
||||
$host = $parsedHostValue['host'];
|
||||
$port = (string)$parsedHostValue['port'];
|
||||
}
|
||||
|
||||
// Allow values that equal the server name
|
||||
// Note that this is only secure if name base virtual host are configured correctly in the webserver
|
||||
$hostMatch = $host === strtolower($serverParams['SERVER_NAME']) && $port === $serverParams['SERVER_PORT'];
|
||||
} else {
|
||||
// In case name based virtual hosts are not possible, we allow setting a trusted host pattern
|
||||
// See https://typo3.org/teams/security/security-bulletins/typo3-core/typo3-core-sa-2014-001/ for further details
|
||||
$hostMatch = (bool)preg_match('/^' . $this->trustedHostsPattern . '$/i', $hostHeaderValue);
|
||||
}
|
||||
|
||||
return $hostMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the webserver uses HTTPS.
|
||||
*
|
||||
* HEADS UP: This does not check if the client performed a
|
||||
* HTTPS request, as possible proxies are not taken into
|
||||
* account. It provides raw information about the current
|
||||
* webservers configuration only.
|
||||
*/
|
||||
protected function webserverUsesHttps(array $serverParams): bool
|
||||
{
|
||||
if (!empty($serverParams['SSL_SESSION_ID'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// https://secure.php.net/manual/en/reserved.variables.server.php
|
||||
// "Set to a non-empty value if the script was queried through the HTTPS protocol."
|
||||
return !empty($serverParams['HTTPS']) && strtolower($serverParams['HTTPS']) !== 'off';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user