218 lines
9.0 KiB
PHP
218 lines
9.0 KiB
PHP
<?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();
|
|
}
|
|
}
|