TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
<?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\Backend\Security\ContentSecurityPolicy;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\Uid\UuidV4;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\NullResponse;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\InvestigateMutationsEvent;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ModelService;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationSuggestion;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportAttribute;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportDemand;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportRepository;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportStatus;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Resolution;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ResolutionRepository;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\SummarizedReport;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
|
||||
|
||||
/**
|
||||
* AJAX endpoint for the CSP backend module, providing access to persisted CSP reports & resolutions.
|
||||
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class CspAjaxController
|
||||
{
|
||||
public function __construct(
|
||||
protected ModelService $modelService,
|
||||
protected PolicyProvider $policyProvider,
|
||||
protected ReportRepository $reportRepository,
|
||||
protected ResolutionRepository $resolutionRepository,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected HashService $hashService,
|
||||
) {}
|
||||
|
||||
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($request->getMethod() === 'GET') {
|
||||
return (new NullResponse())->withStatus(400);
|
||||
}
|
||||
if (!$this->isSystemMaintainer()) {
|
||||
return (new NullResponse())->withStatus(403);
|
||||
}
|
||||
return $this->dispatchAction($request)
|
||||
?? (new NullResponse())->withStatus(500);
|
||||
}
|
||||
|
||||
protected function dispatchAction(ServerRequestInterface $request): ?ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$hmac = $parsedBody['hmac'] ?? null;
|
||||
$action = $parsedBody['action'] ?? null;
|
||||
$summaries = $parsedBody['summaries'] ?? [];
|
||||
$scope = Scope::tryFrom($parsedBody['scope'] ?? '');
|
||||
$uuid = $parsedBody['uuid'] ?? null;
|
||||
if ($uuid !== null) {
|
||||
$uuid = Uuidv4::fromString($uuid);
|
||||
}
|
||||
if (!empty($parsedBody['suggestion'])) {
|
||||
$suggestion = $this->modelService->buildMutationSuggestionFromArray($parsedBody['suggestion']);
|
||||
}
|
||||
// reports
|
||||
if ($action === 'fetchReports') {
|
||||
return $this->fetchReportsAction($scope);
|
||||
}
|
||||
if ($action === 'muteReport' && is_array($summaries)) {
|
||||
return $this->muteReportAction(...$summaries);
|
||||
}
|
||||
if ($action === 'deleteReport' && is_array($summaries)) {
|
||||
return $this->deleteReportAction(...$summaries);
|
||||
}
|
||||
if ($action === 'deleteReports') {
|
||||
return $this->deleteReportsAction($scope);
|
||||
}
|
||||
if ($action === 'handleReport' && $uuid !== null) {
|
||||
return $this->handleReportAction($uuid);
|
||||
}
|
||||
if ($action === 'mutateReport'
|
||||
&& $scope !== null
|
||||
&& is_array($summaries)
|
||||
&& isset($suggestion)
|
||||
&& hash_equals($suggestion->hmac(), $hmac)
|
||||
) {
|
||||
return $this->mutateReportAction($scope, $suggestion, ...$summaries);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function fetchReportsAction(?Scope $scope): ResponseInterface
|
||||
{
|
||||
$demand = ReportDemand::create();
|
||||
$demand->scope = $scope;
|
||||
$reports = $this->reportRepository->findAllSummarized($demand);
|
||||
// @todo not sure whether this is a good idea performance-wise
|
||||
$reports = array_map(
|
||||
function (SummarizedReport $report): SummarizedReport {
|
||||
$event = $this->dispatchInvestigateMutationsEvent($report);
|
||||
if ($event->getMutationSuggestions() !== []) {
|
||||
$mutationHashes = array_map(
|
||||
static fn(MutationSuggestion $suggestion): string => $suggestion->hash(),
|
||||
$event->getMutationSuggestions()
|
||||
);
|
||||
$report = $report->withMutationHashes(...$mutationHashes)
|
||||
->withAttribute(ReportAttribute::fixable);
|
||||
}
|
||||
return $report;
|
||||
},
|
||||
$reports
|
||||
);
|
||||
return new JsonResponse($reports);
|
||||
}
|
||||
|
||||
protected function muteReportAction(string ...$summaries): ResponseInterface
|
||||
{
|
||||
$reports = $this->reportRepository->findBySummary(...$summaries);
|
||||
$uuids = array_map(static fn(Report $report): UuidV4 => $report->uuid, $reports);
|
||||
$this->reportRepository->updateStatus(ReportStatus::Muted, ...$uuids);
|
||||
return new JsonResponse(['uuids' => $uuids]);
|
||||
}
|
||||
|
||||
protected function deleteReportAction(string ...$summaries): ResponseInterface
|
||||
{
|
||||
$reports = $this->reportRepository->findBySummary(...$summaries);
|
||||
$reportUuids = $this->resolveReportUuids(...$reports);
|
||||
$this->reportRepository->updateStatus(ReportStatus::Deleted, ...$reportUuids);
|
||||
return new JsonResponse(['uuids' => $reportUuids]);
|
||||
}
|
||||
|
||||
protected function deleteReportsAction(?Scope $scope): ResponseInterface
|
||||
{
|
||||
$amount = $this->reportRepository->removeAll($scope);
|
||||
return new JsonResponse(['amount' => $amount]);
|
||||
}
|
||||
|
||||
protected function handleReportAction(UuidV4 $uuid): ResponseInterface
|
||||
{
|
||||
$report = $this->reportRepository->findByUuid($uuid);
|
||||
if ($report === null) {
|
||||
return new JsonResponse();
|
||||
}
|
||||
$event = $this->dispatchInvestigateMutationsEvent($report);
|
||||
$suggestions = $event->getMutationSuggestions();
|
||||
// reverse sort by priority (higher priorities take precedence)
|
||||
usort($suggestions, static fn(MutationSuggestion $a, MutationSuggestion $b) => $b->priority <=> $a->priority);
|
||||
return new JsonResponse($suggestions);
|
||||
}
|
||||
|
||||
protected function mutateReportAction(Scope $scope, MutationSuggestion $suggestion, string ...$initiators): ResponseInterface
|
||||
{
|
||||
$summary = $this->generateResolutionSummary($scope, $suggestion);
|
||||
$resolution = $this->resolutionRepository->findBySummary($summary);
|
||||
$reports = $this->reportRepository->findBySummary(...$initiators);
|
||||
if ($resolution !== null || $reports === []) {
|
||||
return new JsonResponse();
|
||||
}
|
||||
$resolution = new Resolution($summary, $scope, $suggestion->identifier, $suggestion->collection, ['initiators' => $initiators]);
|
||||
$this->resolutionRepository->add($resolution);
|
||||
$reportUuids = $this->resolveReportUuids(...$reports);
|
||||
$this->reportRepository->updateStatus(ReportStatus::Handled, ...$reportUuids);
|
||||
return new JsonResponse(['initiators' => $initiators, 'uuids' => $reportUuids]);
|
||||
}
|
||||
|
||||
protected function dispatchInvestigateMutationsEvent(Report $report): InvestigateMutationsEvent
|
||||
{
|
||||
// @todo for future versions, it might be considered to distinguish `enforce` and `report` in the database
|
||||
$policy = $this->policyProvider->provideFor($report->scope, $report->details->resolveDisposition());
|
||||
$event = new InvestigateMutationsEvent($policy, $report);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
return $event;
|
||||
}
|
||||
|
||||
protected function generateResolutionSummary(Scope $scope, MutationSuggestion $suggestion): string
|
||||
{
|
||||
return $this->hashService->hmac(
|
||||
json_encode([
|
||||
$scope,
|
||||
$suggestion->identifier,
|
||||
$suggestion->collection,
|
||||
]),
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
protected function resolveReportUuids(Report ...$reports): array
|
||||
{
|
||||
return array_map(static fn(Report $report): UuidV4 => $report->uuid, $reports);
|
||||
}
|
||||
|
||||
protected function isSystemMaintainer(): bool
|
||||
{
|
||||
$backendUser = $GLOBALS['BE_USER'] ?? null;
|
||||
return $backendUser instanceof BackendUserAuthentication && $backendUser->isSystemMaintainer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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\Backend\Security\ContentSecurityPolicy;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Module\ModuleInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ScopeRepository;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
|
||||
/**
|
||||
* Content-Security-Policy backend module view, loading the CSP lit-element and providing the current context.
|
||||
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class CspModuleController
|
||||
{
|
||||
public function __construct(
|
||||
protected Features $features,
|
||||
protected UriBuilder $uriBuilder,
|
||||
protected PageRenderer $pageRenderer,
|
||||
protected ScopeRepository $scopeRepository,
|
||||
protected ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected IconFactory $iconFactory,
|
||||
protected ComponentFactory $componentFactory,
|
||||
) {}
|
||||
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
$this->registerDocHeaderButtons($view, $request->getAttribute('module'));
|
||||
$view->assignMultiple([
|
||||
'configurationStatus' => $this->getConfigurationStatus(),
|
||||
'scopes' => array_map(strval(...), $this->scopeRepository->findAll()),
|
||||
'controlUri' => $this->uriBuilder->buildUriFromRoutePath('/ajax/security/csp/control'),
|
||||
'extLowlevelAvailable' => ExtensionManagementUtility::isLoaded('lowlevel'),
|
||||
]);
|
||||
return $view->renderResponse('Security/CspModule');
|
||||
}
|
||||
|
||||
protected function registerDocHeaderButtons(ModuleTemplate $view, ModuleInterface $currentModule): void
|
||||
{
|
||||
$view->getDocHeaderComponent()->setShortcutContext(
|
||||
$currentModule->getIdentifier(),
|
||||
$this->getLanguageService()->translate('title', 'backend.modules.content_security_policy')
|
||||
);
|
||||
$view->getDocHeaderComponent()->disableAutomaticReloadButton();
|
||||
$reloadButton = $this->componentFactory
|
||||
->createReloadButton((string)$this->uriBuilder->buildUriFromRoute($currentModule->getIdentifier()))
|
||||
->setDataAttributes(['csp-reports-handler' => 'refresh']);
|
||||
$view->addButtonToButtonBar($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
|
||||
}
|
||||
|
||||
protected function getConfigurationStatus(): array
|
||||
{
|
||||
return [
|
||||
'featureDisabled' => array_filter([
|
||||
'backend' => [],
|
||||
'frontend' => !$this->features->isFeatureEnabled('security.frontend.enforceContentSecurityPolicy')
|
||||
&& !$this->features->isFeatureEnabled('security.frontend.reportContentSecurityPolicy')
|
||||
? ['enforce', 'report']
|
||||
: [],
|
||||
]),
|
||||
'customReporting' => array_filter([
|
||||
'BE' => $GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] ?? '',
|
||||
'FE' => $GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl'] ?? '',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user