TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<?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\Security\ContentSecurityPolicy\Processing;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\InvestigateMutationsEvent;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationSuggestion;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
|
||||
|
||||
/**
|
||||
* Suggest potential resolutions for simple asset violations, e.g.
|
||||
* in case https://example.org/file.js could was blocked to be loaded,
|
||||
* it would exactly suggest this mutation for the given directive.
|
||||
*/
|
||||
class AssetHandler
|
||||
{
|
||||
use HandlerTrait;
|
||||
|
||||
#[AsEventListener('security-csp-asset-handler')]
|
||||
public function __invoke(InvestigateMutationsEvent $event): void
|
||||
{
|
||||
// skip, in case there are mutations already
|
||||
if ($event->getMutationSuggestions() !== []) {
|
||||
return;
|
||||
}
|
||||
$effectiveDirective = $this->resolveEffectiveDirective($event->report);
|
||||
$blockedUri = $this->resolveBlockedUri($event->report);
|
||||
// skip in case `blocked-uri` is not actually a URI with hostname,
|
||||
// or directive is not reasonable to be mutated at all
|
||||
if ($effectiveDirective === null
|
||||
|| $blockedUri === null
|
||||
|| $blockedUri->getHost() === ''
|
||||
|| !$effectiveDirective->isMutationReasonable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->appendMutationSuggestions(
|
||||
...$this->createSuggestions($effectiveDirective, $blockedUri)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<MutationSuggestion>
|
||||
*/
|
||||
private function createSuggestions(Directive $effectiveDirective, UriInterface $blockedUri): array
|
||||
{
|
||||
// @todo resolve URLs to current scope to 'self' instead of using the URL
|
||||
|
||||
$suggestions = [];
|
||||
$hostUri = $blockedUri->withUserInfo('')->withQuery('')->withFragment('');
|
||||
// resolves 'https://example.org/'
|
||||
if ($hostUri->getScheme() !== '') {
|
||||
$suggestions[] = new MutationSuggestion(
|
||||
$this->createExtendingMutationCollection(
|
||||
$effectiveDirective,
|
||||
UriValue::fromUri($hostUri->withPath(''))
|
||||
),
|
||||
self::class . '@hostWithScheme',
|
||||
3,
|
||||
'Assets from host'
|
||||
);
|
||||
}
|
||||
// resolves 'https://example.org/path/to/resource.js'
|
||||
if ($hostUri->getScheme() !== '' && $hostUri->getPath() !== '') {
|
||||
$suggestions[] = new MutationSuggestion(
|
||||
$this->createExtendingMutationCollection(
|
||||
$effectiveDirective,
|
||||
UriValue::fromUri($hostUri)
|
||||
),
|
||||
self::class . '@completeUrl',
|
||||
2,
|
||||
'Asset from specific URL'
|
||||
);
|
||||
}
|
||||
// resolves '*.example.org'
|
||||
$suggestions[] = new MutationSuggestion(
|
||||
$this->createExtendingMutationCollection(
|
||||
$effectiveDirective,
|
||||
new UriValue('*.' . $hostUri->getHost())
|
||||
),
|
||||
self::class . '@wildcardHost',
|
||||
1,
|
||||
'Asset from wildcard host'
|
||||
);
|
||||
return $suggestions;
|
||||
}
|
||||
|
||||
private function createExtendingMutationCollection(Directive $effectiveDirective, UriValue $value): MutationCollection
|
||||
{
|
||||
return new MutationCollection(
|
||||
new Mutation(MutationMode::Extend, $effectiveDirective, $value)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?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\Security\ContentSecurityPolicy\Processing;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\InvestigateMutationsEvent;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationSuggestion;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
|
||||
|
||||
/**
|
||||
* Suggests resolutions for Google-specific assets (e.g. Google Maps JS API).
|
||||
*/
|
||||
class GoogleMapsHandler
|
||||
{
|
||||
use HandlerTrait;
|
||||
|
||||
private const array DOMAIN_NAMES = [
|
||||
'fonts.googleapis.com',
|
||||
'maps.googleapis.com',
|
||||
'fonts.gstatic.com',
|
||||
'maps.gstatic.com',
|
||||
];
|
||||
|
||||
private static MutationSuggestion $suggestion;
|
||||
private static Policy $policyNarrative;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!isset(self::$suggestion)) {
|
||||
self::$suggestion = $this->createGoogleMapsSuggestion();
|
||||
self::$policyNarrative = (new Policy())->mutate(self::$suggestion->collection);
|
||||
}
|
||||
}
|
||||
|
||||
#[AsEventListener('security-csp-google-maps-handler')]
|
||||
public function __invoke(InvestigateMutationsEvent $event): void
|
||||
{
|
||||
$effectiveDirective = $this->resolveEffectiveDirective($event->report);
|
||||
$blockedUri = $this->resolveBlockedUri($event->report);
|
||||
if ($effectiveDirective === null || $blockedUri === null || !$this->isInDomainNames($blockedUri->getHost())) {
|
||||
return;
|
||||
}
|
||||
// skip other handlers
|
||||
$event->stopPropagation();
|
||||
// clear mutations in case a resolution is contained (without inference) in current policy already
|
||||
if ($event->policy->contains(self::$policyNarrative)) {
|
||||
$event->setMutationSuggestions();
|
||||
return;
|
||||
}
|
||||
// otherwise create mutations for Google Maps JS API,
|
||||
// in case the policy narrative would cover (with inference) the current violation
|
||||
if (self::$policyNarrative->coversDirective($effectiveDirective, UriValue::fromUri($blockedUri))) {
|
||||
// override existing mutations (this handler seems to be more specific)
|
||||
$event->setMutationSuggestions(self::$suggestion);
|
||||
}
|
||||
}
|
||||
|
||||
private function createGoogleMapsSuggestion(): MutationSuggestion
|
||||
{
|
||||
// see https://developers.google.com/maps/documentation/javascript/content-security-policy
|
||||
// @todo `Note that 'strict-dynamic' is present, so host-based allowlisting is disabled.`
|
||||
$collection = new MutationCollection(
|
||||
new Mutation(
|
||||
MutationMode::Extend,
|
||||
Directive::ScriptSrcElem,
|
||||
SourceKeyword::strictDynamic, // requires(!) Nonce everywhere
|
||||
SourceScheme::https, // thx Google!
|
||||
SourceKeyword::unsafeEval, // thx Google!
|
||||
SourceScheme::blob, // thx Google!
|
||||
),
|
||||
new Mutation(
|
||||
MutationMode::Extend,
|
||||
Directive::ImgSrc,
|
||||
// @todo should be UriValue (which currently does not support scheme wildcards)
|
||||
new UriValue('https://*.googleapis.com'),
|
||||
new UriValue('https://*.gstatic.com'),
|
||||
new UriValue('*.google.com'),
|
||||
new UriValue('*.googleusercontent.com'),
|
||||
),
|
||||
new Mutation(
|
||||
MutationMode::Extend,
|
||||
Directive::FrameSrc,
|
||||
new UriValue('*.google.com'),
|
||||
),
|
||||
new Mutation(
|
||||
MutationMode::Extend,
|
||||
Directive::ConnectSrc,
|
||||
new UriValue('*.google.com'),
|
||||
new UriValue('https://*.googleapis.com'),
|
||||
new UriValue('https://*.gstatic.com'),
|
||||
SourceScheme::blob, // thx Google!
|
||||
SourceScheme::data, // thx Google!
|
||||
),
|
||||
new Mutation(
|
||||
MutationMode::Extend,
|
||||
Directive::FontSrc,
|
||||
new UriValue('https://fonts.gstatic.com'),
|
||||
),
|
||||
new Mutation(
|
||||
MutationMode::Extend,
|
||||
Directive::StyleSrcElem,
|
||||
SourceKeyword::nonceProxy,
|
||||
new UriValue('https://fonts.gstatic.com'),
|
||||
new UriValue('https://fonts.googleapis.com'),
|
||||
),
|
||||
new Mutation(
|
||||
MutationMode::Extend,
|
||||
Directive::WorkerSrc,
|
||||
SourceScheme::blob,
|
||||
),
|
||||
);
|
||||
return new MutationSuggestion($collection, self::class, 5, 'Google Maps');
|
||||
}
|
||||
|
||||
private function isInDomainNames(string $hostName): bool
|
||||
{
|
||||
if (in_array($hostName, self::DOMAIN_NAMES, true)) {
|
||||
return true;
|
||||
}
|
||||
foreach (self::DOMAIN_NAMES as $domainName) {
|
||||
if (str_ends_with($hostName, '.' . $domainName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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\Security\ContentSecurityPolicy\Processing;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use TYPO3\CMS\Core\Http\Uri;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;
|
||||
|
||||
trait HandlerTrait
|
||||
{
|
||||
private function resolveBlockedUri(Report $report): ?UriInterface
|
||||
{
|
||||
try {
|
||||
return new Uri($report?->details['blocked-uri'] ?? '');
|
||||
} catch (\InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `violatedDirective` is a historical alias of `effectiveDirective`
|
||||
* see https://www.w3.org/TR/CSP3/#violation-events
|
||||
*/
|
||||
private function resolveEffectiveDirective(Report $report): ?Directive
|
||||
{
|
||||
return Directive::tryFrom($report?->details['effective-directive'] ?? '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user