Skip to content

Commit 3d1bff6

Browse files
tgalopinwouterj
authored andcommitted
Integrate the HtmlSanitizer component to the FrameworkBundle
1 parent 0b1f731 commit 3d1bff6

File tree

9 files changed

+465
-0
lines changed

9 files changed

+465
-0
lines changed

DependencyInjection/Configuration.php

Lines changed: 121 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,123 @@ 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+
->normalizeKeys(false)
2127+
->useAttributeAsKey('name')
2128+
->arrayPrototype()
2129+
->children()
2130+
->booleanNode('allow_safe_elements')
2131+
->info('Allows "safe" elements and attributes.')
2132+
->defaultFalse()
2133+
->end()
2134+
->booleanNode('allow_all_static_elements')
2135+
->info('Allows all static elements and attributes from the W3C Sanitizer API standard.')
2136+
->defaultFalse()
2137+
->end()
2138+
->arrayNode('allow_elements')
2139+
->info('Configures elements as allowed. Allowed elements are elements the sanitizer should retain from the input.')
2140+
->fixXmlConfig('allow-element')
2141+
->normalizeKeys(false)
2142+
->useAttributeAsKey('name')
2143+
->variablePrototype()->end()
2144+
->end()
2145+
->arrayNode('block_elements')
2146+
->info('Configures elements as blocked. Blocked elements are elements the sanitizer should remove from the input, but retain their children.')
2147+
->fixXmlConfig('block-element')
2148+
->scalarPrototype()->end()
2149+
->end()
2150+
->arrayNode('drop_elements')
2151+
->info('Configures elements as dropped. Dropped elements are elements the sanitizer should remove from the input, including their children.')
2152+
->fixXmlConfig('drop-element')
2153+
->scalarPrototype()->end()
2154+
->end()
2155+
->arrayNode('allow_attributes')
2156+
->info('Configures attributes as allowed. Allowed attributes are attributes the sanitizer should retain from the input.')
2157+
->fixXmlConfig('allow-attribute')
2158+
->normalizeKeys(false)
2159+
->useAttributeAsKey('name')
2160+
->variablePrototype()->end()
2161+
->end()
2162+
->arrayNode('drop_attributes')
2163+
->info('Configures attributes as dropped. Dropped attributes are attributes the sanitizer should remove from the input.')
2164+
->fixXmlConfig('drop-attribute')
2165+
->normalizeKeys(false)
2166+
->useAttributeAsKey('name')
2167+
->variablePrototype()->end()
2168+
->end()
2169+
->arrayNode('force_attributes')
2170+
->info('Forcefully set the values of certain attributes on certain elements.')
2171+
->fixXmlConfig('force-attribute')
2172+
->normalizeKeys(false)
2173+
->useAttributeAsKey('name')
2174+
->arrayPrototype()
2175+
->normalizeKeys(false)
2176+
->useAttributeAsKey('name')
2177+
->scalarPrototype()->end()
2178+
->end()
2179+
->end()
2180+
->booleanNode('force_https_urls')
2181+
->info('Transforms URLs using the HTTP scheme to use the HTTPS scheme instead.')
2182+
->defaultFalse()
2183+
->end()
2184+
->arrayNode('allowed_link_schemes')
2185+
->info('Allows only a given list of schemes to be used in links href attributes.')
2186+
->fixXmlConfig('allow-link-scheme')
2187+
->scalarPrototype()->end()
2188+
->end()
2189+
->arrayNode('allowed_link_hosts')
2190+
->info('Allows only a given list of hosts to be used in links href attributes.')
2191+
->fixXmlConfig('allow-link-host')
2192+
->scalarPrototype()->end()
2193+
->end()
2194+
->booleanNode('allow_relative_links')
2195+
->info('Allows relative URLs to be used in links href attributes.')
2196+
->defaultFalse()
2197+
->end()
2198+
->arrayNode('allowed_media_schemes')
2199+
->info('Allows only a given list of schemes to be used in media source attributes (img, audio, video, ...).')
2200+
->fixXmlConfig('allow-media-scheme')
2201+
->scalarPrototype()->end()
2202+
->end()
2203+
->arrayNode('allowed_media_hosts')
2204+
->info('Allows only a given list of hosts to be used in media source attributes (img, audio, video, ...).')
2205+
->fixXmlConfig('allow-media-host')
2206+
->scalarPrototype()->end()
2207+
->end()
2208+
->booleanNode('allow_relative_medias')
2209+
->info('Allows relative URLs to be used in media source attributes (img, audio, video, ...).')
2210+
->defaultFalse()
2211+
->end()
2212+
->arrayNode('with_attribute_sanitizers')
2213+
->info('Registers custom attribute sanitizers.')
2214+
->fixXmlConfig('with-attribute-sanitizer')
2215+
->scalarPrototype()->end()
2216+
->end()
2217+
->arrayNode('without_attribute_sanitizers')
2218+
->info('Unregisters custom attribute sanitizers.')
2219+
->fixXmlConfig('without-attribute-sanitizer')
2220+
->scalarPrototype()->end()
2221+
->end()
2222+
->end()
2223+
->end()
2224+
->end()
2225+
->end()
2226+
->end()
2227+
->end()
2228+
;
2229+
}
21092230
}

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;

Resources/config/html_sanitizer.php

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 <[email protected]>
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+
};

Resources/config/schema/symfony-1.0.xsd

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<xsd:element name="rate-limiter" type="rate_limiter" minOccurs="0" maxOccurs="1" />
4040
<xsd:element name="uid" type="uid" minOccurs="0" maxOccurs="1" />
4141
<xsd:element name="notifier" type="notifier" minOccurs="0" maxOccurs="1" />
42+
<xsd:element name="html-sanitizer" type="html-sanitizer" minOccurs="0" maxOccurs="1" />
4243
<xsd:element name="enabled-locale" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
4344
</xsd:choice>
4445

@@ -819,4 +820,63 @@
819820
<xsd:attribute name="email" type="xsd:string" use="required" />
820821
<xsd:attribute name="phone" type="xsd:string" />
821822
</xsd:complexType>
823+
824+
<xsd:complexType name="html-sanitizer">
825+
<xsd:sequence>
826+
<xsd:element name="sanitizer" type="sanitizer" minOccurs="0" maxOccurs="unbounded" />
827+
</xsd:sequence>
828+
<xsd:attribute name="enabled" type="xsd:boolean" />
829+
<xsd:attribute name="default" type="xsd:string" />
830+
</xsd:complexType>
831+
832+
<xsd:complexType name="sanitizer">
833+
<xsd:sequence>
834+
<xsd:element name="allow-element" type="allow-element" minOccurs="0" maxOccurs="unbounded" />
835+
</xsd:sequence>
836+
<xsd:sequence>
837+
<xsd:element name="block-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
838+
</xsd:sequence>
839+
<xsd:sequence>
840+
<xsd:element name="drop-element" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
841+
</xsd:sequence>
842+
<xsd:sequence>
843+
<xsd:element name="allow-attribute" type="allow-attribute" minOccurs="0" maxOccurs="unbounded" />
844+
</xsd:sequence>
845+
<xsd:sequence>
846+
<xsd:element name="drop-attribute" type="drop-attribute" minOccurs="0" maxOccurs="unbounded" />
847+
</xsd:sequence>
848+
<xsd:sequence>
849+
<xsd:element name="force-attribute" type="force-attribute" minOccurs="0" maxOccurs="unbounded" />
850+
</xsd:sequence>
851+
<xsd:sequence>
852+
<xsd:element name="allowed-link-scheme" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
853+
</xsd:sequence>
854+
<xsd:sequence>
855+
<xsd:element name="allowed-link-host" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
856+
</xsd:sequence>
857+
<xsd:sequence>
858+
<xsd:element name="allowed-media-scheme" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
859+
</xsd:sequence>
860+
<xsd:sequence>
861+
<xsd:element name="allowed-media-host" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
862+
</xsd:sequence>
863+
<xsd:sequence>
864+
<xsd:element name="with-attribute-sanitizer" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
865+
</xsd:sequence>
866+
<xsd:sequence>
867+
<xsd:element name="without-attribute-sanitizer" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
868+
</xsd:sequence>
869+
<xsd:attribute name="allow-safe-elements" type="xsd:boolean" />
870+
<xsd:attribute name="allow-all-static-elements" type="xsd:boolean" />
871+
<xsd:attribute name="force-https-urls" type="xsd:boolean" />
872+
<xsd:attribute name="allow-relative-links" type="xsd:boolean" />
873+
<xsd:attribute name="allow-relative-medias" type="xsd:boolean" />
874+
</xsd:complexType>
875+
876+
<xsd:complexType name="allow-element">
877+
<xsd:sequence>
878+
<xsd:element name="attribute" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
879+
</xsd:sequence>
880+
<xsd:attribute name="name" type="xsd:string" />
881+
</xsd:complexType>
822882
</xsd:schema>

0 commit comments

Comments
 (0)