From 71bc5d6fd6f24495a9a5723d8152e7a561e3d013 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 4 Jul 2024 13:02:35 +0200 Subject: [PATCH 1/6] feat(HtmlSanitizer::Config): allow default action from semantic configuration --- .../FrameworkBundle/DependencyInjection/Configuration.php | 5 +++++ .../DependencyInjection/FrameworkExtension.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index d5137dc2ba805..930e0faaeaaee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Form\Form; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerAction; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Cookie; @@ -2382,6 +2383,10 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->fixXmlConfig('with_attribute_sanitizer') ->fixXmlConfig('without_attribute_sanitizer') ->children() + ->enumNode('default_action') + ->info('Defines how the sanitizer must behave by default.') + ->values(array_map(static fn (HtmlSanitizerAction $action): string => $action->value, HtmlSanitizerAction::cases())) + ->end() ->booleanNode('allow_safe_elements') ->info('Allows "safe" elements and attributes.') ->defaultFalse() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 7cc67725ec461..b02876c6b873c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -80,6 +80,7 @@ use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerAction; use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\Messenger\PingWebhookMessageHandler; @@ -3006,6 +3007,10 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil $def = $container->register($configId, HtmlSanitizerConfig::class); // Base + if ($sanitizerConfig['default_action']) { + $def->addMethodCall('defaultAction', [HtmlSanitizerAction::from($sanitizerConfig['default_action'])], true); + } + if ($sanitizerConfig['allow_safe_elements']) { $def->addMethodCall('allowSafeElements', [], true); } From 8b9f9f4e278a94467393213b077ef3103e896763 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 4 Jul 2024 18:04:43 +0200 Subject: [PATCH 2/6] [UPDATE] Check for version installed of sanitizer --- .../DependencyInjection/FrameworkExtension.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index b02876c6b873c..12e1b0bc1e7a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Composer\InstalledVersions; +use Composer\Semver\VersionParser; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use phpDocumentor\Reflection\DocBlockFactoryInterface; @@ -3008,6 +3009,13 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil // Base if ($sanitizerConfig['default_action']) { + if (InstalledVersions::satisfies(new VersionParser(), 'symfony/html-sanitizer', '>=7.2') === false) { + throw new LogicException(\sprintf( + 'Default action requires the HtmlSanitizer component to be installed in version >=7.2 (%s currently installed). Try running "composer require symfony/html-sanitizer".', + InstalledVersions::getVersion('symfony/html-sanitizer') + )); + } + $def->addMethodCall('defaultAction', [HtmlSanitizerAction::from($sanitizerConfig['default_action'])], true); } From 29f97495ae41ea52acc4523ec8e271c596ac1e43 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 4 Jul 2024 18:12:04 +0200 Subject: [PATCH 3/6] [FIX] Key might be missing --- .../FrameworkBundle/DependencyInjection/FrameworkExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 12e1b0bc1e7a7..3aa273f358283 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -3008,7 +3008,7 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil $def = $container->register($configId, HtmlSanitizerConfig::class); // Base - if ($sanitizerConfig['default_action']) { + if ($sanitizerConfig['default_action'] ?? false) { if (InstalledVersions::satisfies(new VersionParser(), 'symfony/html-sanitizer', '>=7.2') === false) { throw new LogicException(\sprintf( 'Default action requires the HtmlSanitizer component to be installed in version >=7.2 (%s currently installed). Try running "composer require symfony/html-sanitizer".', From d63e583acf5cd633fd7052192ed30586a698c31e Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 5 Jul 2024 11:04:12 +0200 Subject: [PATCH 4/6] [UPDATE] Add some tests for default_action --- .../FrameworkExtension.php | 3 +- .../Fixtures/php/html_sanitizer.php | 1 + .../html_sanitizer_without_default_action.php | 50 +++++++++++++ .../Fixtures/xml/html_sanitizer.xml | 1 + .../html_sanitizer_without_default_action.xml | 65 +++++++++++++++++ .../Fixtures/yml/html_sanitizer.yml | 1 + .../html_sanitizer_without_default_action.yml | 44 ++++++++++++ .../FrameworkExtensionTestCase.php | 71 ++++++++++++++++++- 8 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer_without_default_action.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer_without_default_action.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer_without_default_action.yml diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 3aa273f358283..5cc7ee8fc9c9c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Composer\InstalledVersions; -use Composer\Semver\VersionParser; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use phpDocumentor\Reflection\DocBlockFactoryInterface; @@ -3009,7 +3008,7 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil // Base if ($sanitizerConfig['default_action'] ?? false) { - if (InstalledVersions::satisfies(new VersionParser(), 'symfony/html-sanitizer', '>=7.2') === false) { + if (!class_exists(HtmlSanitizerAction::class)) { throw new LogicException(\sprintf( 'Default action requires the HtmlSanitizer component to be installed in version >=7.2 (%s currently installed). Try running "composer require symfony/html-sanitizer".', InstalledVersions::getVersion('symfony/html-sanitizer') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php index 1b64cd1b3e21e..81ae3c337cec0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer.php @@ -8,6 +8,7 @@ 'html_sanitizer' => [ 'sanitizers' => [ 'custom' => [ + 'default_action' => 'allow', 'allow_safe_elements' => true, 'allow_static_elements' => true, 'allow_elements' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer_without_default_action.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer_without_default_action.php new file mode 100644 index 0000000000000..1b64cd1b3e21e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/html_sanitizer_without_default_action.php @@ -0,0 +1,50 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'html_sanitizer' => [ + 'sanitizers' => [ + 'custom' => [ + 'allow_safe_elements' => true, + 'allow_static_elements' => true, + 'allow_elements' => [ + 'iframe' => 'src', + 'custom-tag' => ['data-attr', 'data-attr-1'], + 'custom-tag-2' => '*', + ], + 'block_elements' => ['section'], + 'drop_elements' => ['video'], + 'allow_attributes' => [ + 'src' => ['iframe'], + 'data-attr' => '*', + ], + 'drop_attributes' => [ + 'data-attr' => ['custom-tag'], + 'data-attr-1' => [], + 'data-attr-2' => '*', + ], + '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, + 'with_attribute_sanitizers' => [ + 'App\\Sanitizer\\CustomAttributeSanitizer', + ], + 'without_attribute_sanitizers' => [ + 'App\\Sanitizer\\OtherCustomAttributeSanitizer', + ], + ], + 'all.sanitizer' => null, + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml index 7cb6758d93bfe..ce97226fc3fd3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/html_sanitizer.xml @@ -10,6 +10,7 @@ + + + + + + + + + + src + + + data-attr + data-attr-1 + + + * + + section + video + + iframe + + + * + + + custom-tag + + + + * + + + noopener noreferrer + + + bp4-heading + + http + https + mailto + symfony.com + http + https + data + symfony.com + App\Sanitizer\CustomAttributeSanitizer + App\Sanitizer\OtherCustomAttributeSanitizer + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml index f0d515e418d86..77aae692a03fc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer.yml @@ -7,6 +7,7 @@ framework: html_sanitizer: sanitizers: custom: + default_action: 'allow' allow_safe_elements: true allow_static_elements: true allow_elements: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer_without_default_action.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer_without_default_action.yml new file mode 100644 index 0000000000000..f0d515e418d86 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/html_sanitizer_without_default_action.yml @@ -0,0 +1,44 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + html_sanitizer: + sanitizers: + custom: + allow_safe_elements: true + allow_static_elements: true + allow_elements: + iframe: 'src' + custom-tag: ['data-attr', 'data-attr-1'] + custom-tag-2: '*' + block_elements: + - section + drop_elements: + - video + allow_attributes: + src: ['iframe'] + data-attr: '*' + drop_attributes: + data-attr: [custom-tag] + data-attr-1: [] + data-attr-2: '*' + 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 + with_attribute_sanitizers: + - App\Sanitizer\CustomAttributeSanitizer + without_attribute_sanitizers: + - App\Sanitizer\OtherCustomAttributeSanitizer + + all.sanitizer: null diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index b37d2e910ec45..d5f91e19e166a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -47,6 +47,7 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\Form\Form; use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerAction; use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\MockHttpClient; @@ -2219,8 +2220,74 @@ public function testLocaleSwitcherServiceRegistered() $this->assertNotContains('translation.locale_switcher', $localeAwareServices); } + public function testHtmlSanitizerBefore72() + { + if (class_exists(HtmlSanitizerAction::class)) { + $this->markTestSkipped('HtmlSanitizer version is <7.2'); + } + + $container = $this->createContainerFromFile('html_sanitizer_without_default_action'); + + // html_sanitizer service + $this->assertSame(HtmlSanitizer::class, $container->getDefinition('html_sanitizer.sanitizer.custom')->getClass()); + $this->assertCount(1, $args = $container->getDefinition('html_sanitizer.sanitizer.custom')->getArguments()); + $this->assertSame('html_sanitizer.config.custom', (string) $args[0]); + + // config + $this->assertTrue($container->hasDefinition('html_sanitizer.config.custom'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer'); + $this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config.custom')->getClass()); + $this->assertCount(23, $calls = $container->getDefinition('html_sanitizer.config.custom')->getMethodCalls()); + $this->assertSame( + [ + ['allowSafeElements', [], true], + ['allowStaticElements', [], true], + ['allowElement', ['iframe', 'src'], true], + ['allowElement', ['custom-tag', ['data-attr', 'data-attr-1']], true], + ['allowElement', ['custom-tag-2', '*'], true], + ['blockElement', ['section'], true], + ['dropElement', ['video'], true], + ['allowAttribute', ['src', $this instanceof XmlFrameworkExtensionTest ? 'iframe' : ['iframe']], true], + ['allowAttribute', ['data-attr', '*'], true], + ['dropAttribute', ['data-attr', $this instanceof XmlFrameworkExtensionTest ? 'custom-tag' : ['custom-tag']], true], + ['dropAttribute', ['data-attr-1', []], true], + ['dropAttribute', ['data-attr-2', '*'], true], + ['forceAttribute', ['a', 'rel', 'noopener noreferrer'], true], + ['forceAttribute', ['h1', 'class', 'bp4-heading'], true], + ['forceHttpsUrls', [true], true], + ['allowLinkSchemes', [['http', 'https', 'mailto']], true], + ['allowLinkHosts', [['symfony.com']], true], + ['allowRelativeLinks', [true], true], + ['allowMediaSchemes', [['http', 'https', 'data']], true], + ['allowMediaHosts', [['symfony.com']], true], + ['allowRelativeMedias', [true], true], + ['withAttributeSanitizer', ['@App\\Sanitizer\\CustomAttributeSanitizer'], true], + ['withoutAttributeSanitizer', ['@App\\Sanitizer\\OtherCustomAttributeSanitizer'], true], + ], + + // Convert references to their names for easier assertion + array_map( + static function ($call) { + foreach ($call[1] as $k => $arg) { + $call[1][$k] = $arg instanceof Reference ? '@'.$arg : $arg; + } + + return $call; + }, + $calls + ) + ); + + // Named alias + $this->assertSame('html_sanitizer.sanitizer.all.sanitizer', (string) $container->getAlias(HtmlSanitizerInterface::class.' $allSanitizer')); + $this->assertFalse($container->hasAlias(HtmlSanitizerInterface::class.' $default')); + } + public function testHtmlSanitizer() { + if (!class_exists(HtmlSanitizerAction::class)) { + $this->markTestSkipped('HtmlSanitizer version must be >=7.2'); + } + $container = $this->createContainerFromFile('html_sanitizer'); // html_sanitizer service @@ -2231,9 +2298,11 @@ public function testHtmlSanitizer() // config $this->assertTrue($container->hasDefinition('html_sanitizer.config.custom'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer'); $this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config.custom')->getClass()); - $this->assertCount(23, $calls = $container->getDefinition('html_sanitizer.config.custom')->getMethodCalls()); + $calls = $container->getDefinition('html_sanitizer.config.custom')->getMethodCalls(); + $this->assertCount(24, $calls = $container->getDefinition('html_sanitizer.config.custom')->getMethodCalls()); $this->assertSame( [ + ['defaultAction', [HtmlSanitizerAction::Allow], true], ['allowSafeElements', [], true], ['allowStaticElements', [], true], ['allowElement', ['iframe', 'src'], true], From 4744fe20bb4aa49f5871e2ce109d220f03ee6f1b Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 5 Jul 2024 11:05:52 +0200 Subject: [PATCH 5/6] [UPDATE] Remove duplicate code test --- .../Tests/DependencyInjection/FrameworkExtensionTestCase.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index d5f91e19e166a..72a8bbf0b0508 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -2298,7 +2298,6 @@ public function testHtmlSanitizer() // config $this->assertTrue($container->hasDefinition('html_sanitizer.config.custom'), '->registerHtmlSanitizerConfiguration() loads custom sanitizer'); $this->assertSame(HtmlSanitizerConfig::class, $container->getDefinition('html_sanitizer.config.custom')->getClass()); - $calls = $container->getDefinition('html_sanitizer.config.custom')->getMethodCalls(); $this->assertCount(24, $calls = $container->getDefinition('html_sanitizer.config.custom')->getMethodCalls()); $this->assertSame( [ From 6a05b1b72fdc7d01065f1c34854f75dd8114f157 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Tue, 9 Jul 2024 20:46:03 +0200 Subject: [PATCH 6/6] [UPDATE] Fix xml config --- .../FrameworkBundle/Resources/config/schema/symfony-1.0.xsd | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index d8d23168d1887..55935d2d5fbbd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -918,6 +918,7 @@ +