Skip to content

Commit 8064a5c

Browse files
committed
feature #44798 [FrameworkBundle] Integrate the HtmlSanitizer component (tgalopin, wouterj)
This PR was merged into the 6.1 branch. Discussion ---------- [FrameworkBundle] Integrate the HtmlSanitizer component | Q | A | ------------- | --- | Branch? | 6.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This PR adds the integration if the HtmlSanitizer component in the FrameworkBundle. See #44681 for details about the component. The configuration for this integration is the following: ```yaml framework: # This configuration is not required: as soon as you install the component, a default # "html_sanitizer" service is created, with the safe configuration, to be used directly. # # This configuration allows to set custom behaviors, in addition or instead of the default. html_sanitizer: # Default sanitizer (optional) # When not provided, the native "html_sanitizer" service is wired as default. default: my.sanitizer # Custom sanitizers (optional) sanitizers: # Each sanitizer defines its own service (no prefix/suffix) to ease understanding # Each sanitizer also defines a named autowiring alias to ease injection using variable name. # Here, this sanitizer is available as a service "my.sanitizer" or using autowiring # as "HtmlSanitizerInterface $mySanitizer". my.sanitizer: allow_safe_elements: true allow_elements: iframe: ['src'] custom-tag: ['data-attr'] custom-tag-2: '*' block_elements: - section drop_elements: - video allow_attributes: src: ['iframe'] data-attr: '*' drop_attributes: data-attr: '*' force_attributes: a: rel: noopener noreferrer h1: class: bp4-heading force_https_urls: true allowed_link_schemes: ['http', 'https', 'mailto'] allowed_link_hosts: ['symfony.com'] allow_relative_links: true allowed_media_schemes: ['http', 'https', 'data'] allowed_media_hosts: ['symfony.com'] allow_relative_medias: true # "all.sanitizer" / "HtmlSanitizerInterface $allSanitizer" all.sanitizer: allow_all_static_elements: true allow_elements: custom-tag: ['data-attr'] ``` This PR is still WIP (esp tests) but I wanted to gather feedback regarding the configuration and DX as soon as possible. Commits ------- 4dd3fd6 Finished XML config implementation e0a9339 Integrate the HtmlSanitizer component to the FrameworkBundle
2 parents ed382fc + 4dd3fd6 commit 8064a5c

File tree

11 files changed

+554
-0
lines changed

11 files changed

+554
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\DependencyInjection\ContainerBuilder;
2626
use Symfony\Component\DependencyInjection\Exception\LogicException;
2727
use Symfony\Component\Form\Form;
28+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
2829
use Symfony\Component\HttpClient\HttpClient;
2930
use Symfony\Component\HttpFoundation\Cookie;
3031
use Symfony\Component\Lock\Lock;
@@ -167,6 +168,7 @@ public function getConfigTreeBuilder(): TreeBuilder
167168
$this->addNotifierSection($rootNode, $enableIfStandalone);
168169
$this->addRateLimiterSection($rootNode, $enableIfStandalone);
169170
$this->addUidSection($rootNode, $enableIfStandalone);
171+
$this->addHtmlSanitizerSection($rootNode, $enableIfStandalone);
170172

171173
return $treeBuilder;
172174
}
@@ -2106,4 +2108,147 @@ private function addUidSection(ArrayNodeDefinition $rootNode, callable $enableIf
21062108
->end()
21072109
;
21082110
}
2111+
2112+
private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
2113+
{
2114+
$rootNode
2115+
->children()
2116+
->arrayNode('html_sanitizer')
2117+
->info('HtmlSanitizer configuration')
2118+
->{$enableIfStandalone('symfony/html-sanitizer', HtmlSanitizerInterface::class)}()
2119+
->fixXmlConfig('sanitizer')
2120+
->children()
2121+
->scalarNode('default')
2122+
->defaultNull()
2123+
->info('Default sanitizer to use when injecting without named binding.')
2124+
->end()
2125+
->arrayNode('sanitizers')
2126+
->useAttributeAsKey('name')
2127+
->arrayPrototype()
2128+
->fixXmlConfig('allow_element')
2129+
->fixXmlConfig('block_element')
2130+
->fixXmlConfig('drop_element')
2131+
->fixXmlConfig('allow_attribute')
2132+
->fixXmlConfig('drop_attribute')
2133+
->fixXmlConfig('force_attribute')
2134+
->fixXmlConfig('allowed_link_scheme')
2135+
->fixXmlConfig('allowed_link_host')
2136+
->fixXmlConfig('allowed_media_scheme')
2137+
->fixXmlConfig('allowed_media_host')
2138+
->fixXmlConfig('with_attribute_sanitizer')
2139+
->fixXmlConfig('without_attribute_sanitizer')
2140+
->children()
2141+
->booleanNode('allow_safe_elements')
2142+
->info('Allows "safe" elements and attributes.')
2143+
->defaultFalse()
2144+
->end()
2145+
->booleanNode('allow_all_static_elements')
2146+
->info('Allows all static elements and attributes from the W3C Sanitizer API standard.')
2147+
->defaultFalse()
2148+
->end()
2149+
->arrayNode('allow_elements')
2150+
->info('Configures the elements that the sanitizer should retain from the input. The element name is the key, the value is either a list of allowed attributes for this element or "*" to allow the default set of attributes (https://wicg.github.io/sanitizer-api/#default-configuration).')
2151+
->example(['i' => '*', 'a' => ['title'], 'span' => 'class'])
2152+
->normalizeKeys(false)
2153+
->useAttributeAsKey('name')
2154+
->variablePrototype()
2155+
->beforeNormalization()
2156+
->ifArray()->then(fn ($n) => $n['attribute'] ?? $n)
2157+
->end()
2158+
->validate()
2159+
->ifTrue(fn ($n): bool => !\is_string($n) && !\is_array($n))
2160+
->thenInvalid('The value must be either a string or an array of strings.')
2161+
->end()
2162+
->end()
2163+
->end()
2164+
->arrayNode('block_elements')
2165+
->info('Configures elements as blocked. Blocked elements are elements the sanitizer should remove from the input, but retain their children.')
2166+
->beforeNormalization()
2167+
->ifString()
2168+
->then(fn (string $n): array => (array) $n)
2169+
->end()
2170+
->scalarPrototype()->end()
2171+
->end()
2172+
->arrayNode('drop_elements')
2173+
->info('Configures elements as dropped. Dropped elements are elements the sanitizer should remove from the input, including their children.')
2174+
->beforeNormalization()
2175+
->ifString()
2176+
->then(fn (string $n): array => (array) $n)
2177+
->end()
2178+
->scalarPrototype()->end()
2179+
->end()
2180+
->arrayNode('allow_attributes')
2181+
->info('Configures attributes as allowed. Allowed attributes are attributes the sanitizer should retain from the input.')
2182+
->normalizeKeys(false)
2183+
->useAttributeAsKey('name')
2184+
->variablePrototype()
2185+
->beforeNormalization()
2186+
->ifArray()->then(fn ($n) => $n['element'] ?? $n)
2187+
->end()
2188+
->end()
2189+
->end()
2190+
->arrayNode('drop_attributes')
2191+
->info('Configures attributes as dropped. Dropped attributes are attributes the sanitizer should remove from the input.')
2192+
->normalizeKeys(false)
2193+
->useAttributeAsKey('name')
2194+
->variablePrototype()
2195+
->beforeNormalization()
2196+
->ifArray()->then(fn ($n) => $n['element'] ?? $n)
2197+
->end()
2198+
->end()
2199+
->end()
2200+
->arrayNode('force_attributes')
2201+
->info('Forcefully set the values of certain attributes on certain elements.')
2202+
->normalizeKeys(false)
2203+
->useAttributeAsKey('name')
2204+
->arrayPrototype()
2205+
->normalizeKeys(false)
2206+
->useAttributeAsKey('name')
2207+
->scalarPrototype()->end()
2208+
->end()
2209+
->end()
2210+
->booleanNode('force_https_urls')
2211+
->info('Transforms URLs using the HTTP scheme to use the HTTPS scheme instead.')
2212+
->defaultFalse()
2213+
->end()
2214+
->arrayNode('allowed_link_schemes')
2215+
->info('Allows only a given list of schemes to be used in links href attributes.')
2216+
->scalarPrototype()->end()
2217+
->end()
2218+
->arrayNode('allowed_link_hosts')
2219+
->info('Allows only a given list of hosts to be used in links href attributes.')
2220+
->scalarPrototype()->end()
2221+
->end()
2222+
->booleanNode('allow_relative_links')
2223+
->info('Allows relative URLs to be used in links href attributes.')
2224+
->defaultFalse()
2225+
->end()
2226+
->arrayNode('allowed_media_schemes')
2227+
->info('Allows only a given list of schemes to be used in media source attributes (img, audio, video, ...).')
2228+
->scalarPrototype()->end()
2229+
->end()
2230+
->arrayNode('allowed_media_hosts')
2231+
->info('Allows only a given list of hosts to be used in media source attributes (img, audio, video, ...).')
2232+
->scalarPrototype()->end()
2233+
->end()
2234+
->booleanNode('allow_relative_medias')
2235+
->info('Allows relative URLs to be used in media source attributes (img, audio, video, ...).')
2236+
->defaultFalse()
2237+
->end()
2238+
->arrayNode('with_attribute_sanitizers')
2239+
->info('Registers custom attribute sanitizers.')
2240+
->scalarPrototype()->end()
2241+
->end()
2242+
->arrayNode('without_attribute_sanitizers')
2243+
->info('Unregisters custom attribute sanitizers.')
2244+
->scalarPrototype()->end()
2245+
->end()
2246+
->end()
2247+
->end()
2248+
->end()
2249+
->end()
2250+
->end()
2251+
->end()
2252+
;
2253+
}
21092254
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
use Symfony\Component\Form\FormTypeExtensionInterface;
7474
use Symfony\Component\Form\FormTypeGuesserInterface;
7575
use Symfony\Component\Form\FormTypeInterface;
76+
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
77+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
78+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
7679
use Symfony\Component\HttpClient\MockHttpClient;
7780
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
7881
use Symfony\Component\HttpClient\RetryableHttpClient;
@@ -531,6 +534,14 @@ public function load(array $configs, ContainerBuilder $container)
531534
// profiler depends on form, validation, translation, messenger, mailer, http-client, notifier, serializer being registered
532535
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
533536

537+
if ($this->isConfigEnabled($container, $config['html_sanitizer'])) {
538+
if (!class_exists(HtmlSanitizerConfig::class)) {
539+
throw new LogicException('HtmlSanitizer support cannot be enabled as the HtmlSanitizer component is not installed. Try running "composer require symfony/html-sanitizer".');
540+
}
541+
542+
$this->registerHtmlSanitizerConfiguration($config['html_sanitizer'], $container, $loader);
543+
}
544+
534545
$this->addAnnotatedClassesToCompile([
535546
'**\\Controller\\',
536547
'**\\Entity\\',
@@ -2659,6 +2670,81 @@ private function registerUidConfiguration(array $config, ContainerBuilder $conta
26592670
}
26602671
}
26612672

2673+
private function registerHtmlSanitizerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
2674+
{
2675+
$loader->load('html_sanitizer.php');
2676+
2677+
foreach ($config['sanitizers'] as $sanitizerName => $sanitizerConfig) {
2678+
$configId = 'html_sanitizer.config.'.$sanitizerName;
2679+
$def = $container->register($configId, HtmlSanitizerConfig::class);
2680+
2681+
// Base
2682+
if ($sanitizerConfig['allow_safe_elements']) {
2683+
$def->addMethodCall('allowSafeElements', [], true);
2684+
}
2685+
2686+
if ($sanitizerConfig['allow_all_static_elements']) {
2687+
$def->addMethodCall('allowAllStaticElements', [], true);
2688+
}
2689+
2690+
// Configures elements
2691+
foreach ($sanitizerConfig['allow_elements'] as $element => $attributes) {
2692+
$def->addMethodCall('allowElement', [$element, $attributes], true);
2693+
}
2694+
2695+
foreach ($sanitizerConfig['block_elements'] as $element) {
2696+
$def->addMethodCall('blockElement', [$element], true);
2697+
}
2698+
2699+
foreach ($sanitizerConfig['drop_elements'] as $element) {
2700+
$def->addMethodCall('dropElement', [$element], true);
2701+
}
2702+
2703+
// Configures attributes
2704+
foreach ($sanitizerConfig['allow_attributes'] as $attribute => $elements) {
2705+
$def->addMethodCall('allowAttribute', [$attribute, $elements], true);
2706+
}
2707+
2708+
foreach ($sanitizerConfig['drop_attributes'] as $attribute => $elements) {
2709+
$def->addMethodCall('dropAttribute', [$attribute, $elements], true);
2710+
}
2711+
2712+
// Force attributes
2713+
foreach ($sanitizerConfig['force_attributes'] as $element => $attributes) {
2714+
foreach ($attributes as $attrName => $attrValue) {
2715+
$def->addMethodCall('forceAttribute', [$element, $attrName, $attrValue], true);
2716+
}
2717+
}
2718+
2719+
// Settings
2720+
$def->addMethodCall('forceHttpsUrls', [$sanitizerConfig['force_https_urls']], true);
2721+
$def->addMethodCall('allowLinkSchemes', [$sanitizerConfig['allowed_link_schemes']], true);
2722+
$def->addMethodCall('allowLinkHosts', [$sanitizerConfig['allowed_link_hosts']], true);
2723+
$def->addMethodCall('allowRelativeLinks', [$sanitizerConfig['allow_relative_links']], true);
2724+
$def->addMethodCall('allowMediaSchemes', [$sanitizerConfig['allowed_media_schemes']], true);
2725+
$def->addMethodCall('allowMediaHosts', [$sanitizerConfig['allowed_media_hosts']], true);
2726+
$def->addMethodCall('allowRelativeMedias', [$sanitizerConfig['allow_relative_medias']], true);
2727+
2728+
// Custom attribute sanitizers
2729+
foreach ($sanitizerConfig['with_attribute_sanitizers'] as $serviceName) {
2730+
$def->addMethodCall('withAttributeSanitizer', [new Reference($serviceName)], true);
2731+
}
2732+
2733+
foreach ($sanitizerConfig['without_attribute_sanitizers'] as $serviceName) {
2734+
$def->addMethodCall('withoutAttributeSanitizer', [new Reference($serviceName)], true);
2735+
}
2736+
2737+
// Create the sanitizer and link its config
2738+
$sanitizerId = 'html_sanitizer.sanitizer.'.$sanitizerName;
2739+
$container->register($sanitizerId, HtmlSanitizer::class)->addArgument(new Reference($configId));
2740+
2741+
$container->registerAliasForArgument($sanitizerId, HtmlSanitizerInterface::class, $sanitizerName);
2742+
}
2743+
2744+
$default = $config['default'] ? 'html_sanitizer.sanitizer.'.$config['default'] : 'html_sanitizer';
2745+
$container->setAlias(HtmlSanitizerInterface::class, new Reference($default));
2746+
}
2747+
26622748
private function resolveTrustedHeaders(array $headers): int
26632749
{
26642750
$trustedHeaders = 0;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
15+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
16+
17+
return static function (ContainerConfigurator $container) {
18+
$container->services()
19+
->set('html_sanitizer.config', HtmlSanitizerConfig::class)
20+
->call('allowSafeElements')
21+
22+
->set('html_sanitizer', HtmlSanitizer::class)
23+
->args([service('html_sanitizer.config')])
24+
;
25+
};

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy