Skip to content

Commit fe8b477

Browse files
committed
[HtmlSanitizer] Add support for configuring the default action to block or allow unconfigured elements instead of dropping them
1 parent e0ad00c commit fe8b477

File tree

6 files changed

+147
-26
lines changed

6 files changed

+147
-26
lines changed

src/Symfony/Component/HtmlSanitizer/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
7.2
5+
---
6+
7+
* Add support for configuring the default action to block or allow unconfigured elements instead of dropping them
8+
9+
410
6.4
511
---
612

src/Symfony/Component/HtmlSanitizer/HtmlSanitizer.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,13 @@ private function createDomVisitorForContext(string $context): DomVisitor
103103

104104
foreach ($this->config->getBlockedElements() as $blockedElement => $v) {
105105
if (\array_key_exists($blockedElement, W3CReference::HEAD_ELEMENTS)) {
106-
$elementsConfig[$blockedElement] = false;
106+
$elementsConfig[$blockedElement] = HtmlSanitizerAction::Block;
107+
}
108+
}
109+
110+
foreach ($this->config->getDroppedElements() as $droppedElement => $v) {
111+
if (\array_key_exists($droppedElement, W3CReference::HEAD_ELEMENTS)) {
112+
$elementsConfig[$droppedElement] = HtmlSanitizerAction::Drop;
107113
}
108114
}
109115

@@ -119,7 +125,13 @@ private function createDomVisitorForContext(string $context): DomVisitor
119125

120126
foreach ($this->config->getBlockedElements() as $blockedElement => $v) {
121127
if (!\array_key_exists($blockedElement, W3CReference::HEAD_ELEMENTS)) {
122-
$elementsConfig[$blockedElement] = false;
128+
$elementsConfig[$blockedElement] = HtmlSanitizerAction::Block;
129+
}
130+
}
131+
132+
foreach ($this->config->getDroppedElements() as $droppedElement => $v) {
133+
if (!\array_key_exists($droppedElement, W3CReference::HEAD_ELEMENTS)) {
134+
$elementsConfig[$droppedElement] = HtmlSanitizerAction::Drop;
123135
}
124136
}
125137

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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\HtmlSanitizer;
13+
14+
enum HtmlSanitizerAction: string
15+
{
16+
// Dropped elements are elements the sanitizer should remove from the input, including their children.
17+
case Drop = 'drop';
18+
19+
// Blocked elements are elements the sanitizer should remove from the input, but retain their children.
20+
case Block = 'block';
21+
22+
// Allowed elements are elements the sanitizer should retain from the input.
23+
case Allow = 'allow';
24+
}

src/Symfony/Component/HtmlSanitizer/HtmlSanitizerConfig.php

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@
1919
*/
2020
class HtmlSanitizerConfig
2121
{
22+
private HtmlSanitizerAction $defaultAction = HtmlSanitizerAction::Drop;
23+
24+
/**
25+
* Elements that should be removed.
26+
*
27+
* @var array<string, true>
28+
*/
29+
private array $droppedElements = [];
30+
2231
/**
2332
* Elements that should be removed but their children should be retained.
2433
*
@@ -99,6 +108,19 @@ public function __construct()
99108
];
100109
}
101110

111+
/**
112+
* Sets the default action for elements which are not otherwise specifically allowed or blocked.
113+
*
114+
* Note that a default action of Allow will allow all tags but they will not have any attributes.
115+
*/
116+
public function defaultAction(HtmlSanitizerAction $action): static
117+
{
118+
$clone = clone $this;
119+
$clone->defaultAction = $action;
120+
121+
return $clone;
122+
}
123+
102124
/**
103125
* Allows all static elements and attributes from the W3C Sanitizer API standard.
104126
*
@@ -112,7 +134,7 @@ public function allowStaticElements(): static
112134
array_keys(W3CReference::BODY_ELEMENTS)
113135
);
114136

115-
$clone = clone $this;
137+
$clone = $this;
116138
foreach ($elements as $element) {
117139
$clone = $clone->allowElement($element, '*');
118140
}
@@ -134,7 +156,7 @@ public function allowSafeElements(): static
134156
}
135157
}
136158

137-
$clone = clone $this;
159+
$clone = $this;
138160

139161
foreach (W3CReference::HEAD_ELEMENTS as $element => $isSafe) {
140162
if ($isSafe) {
@@ -261,8 +283,8 @@ public function allowElement(string $element, array|string $allowedAttributes =
261283
{
262284
$clone = clone $this;
263285

264-
// Unblock the element is necessary
265-
unset($clone->blockedElements[$element]);
286+
// Unblock/undrop the element if necessary
287+
unset($clone->blockedElements[$element], $clone->droppedElements[$element]);
266288

267289
$clone->allowedElements[$element] = [];
268290

@@ -284,8 +306,8 @@ public function blockElement(string $element): static
284306
{
285307
$clone = clone $this;
286308

287-
// Disallow the element is necessary
288-
unset($clone->allowedElements[$element]);
309+
// Disallow/undrop the element if necessary
310+
unset($clone->allowedElements[$element], $clone->droppedElements[$element]);
289311

290312
$clone->blockedElements[$element] = true;
291313

@@ -300,13 +322,15 @@ public function blockElement(string $element): static
300322
*
301323
* Note: when using an empty configuration, all unknown elements are dropped
302324
* automatically. This method let you drop elements that were allowed earlier
303-
* in the configuration.
325+
* in the configuration, or explicitly drop some if you changed the default action.
304326
*/
305327
public function dropElement(string $element): static
306328
{
307329
$clone = clone $this;
308330
unset($clone->allowedElements[$element], $clone->blockedElements[$element]);
309331

332+
$clone->droppedElements[$element] = true;
333+
310334
return $clone;
311335
}
312336

@@ -426,6 +450,11 @@ public function getMaxInputLength(): int
426450
return $this->maxInputLength;
427451
}
428452

453+
public function getDefaultAction(): HtmlSanitizerAction
454+
{
455+
return $this->defaultAction;
456+
}
457+
429458
/**
430459
* @return array<string, array<string, true>>
431460
*/
@@ -442,6 +471,14 @@ public function getBlockedElements(): array
442471
return $this->blockedElements;
443472
}
444473

474+
/**
475+
* @return array<string, true>
476+
*/
477+
public function getDroppedElements(): array
478+
{
479+
return $this->droppedElements;
480+
}
481+
445482
/**
446483
* @return array<string, array<string, string>>
447484
*/

src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerAllTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
16+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerAction;
1617
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
1718

1819
class HtmlSanitizerAllTest extends TestCase
@@ -578,4 +579,25 @@ public function testUnlimitedLength()
578579

579580
$this->assertSame(\strlen($input), \strlen($sanitized));
580581
}
582+
583+
public function testBlockByDefault()
584+
{
585+
$config = (new HtmlSanitizerConfig())
586+
->defaultAction(HtmlSanitizerAction::Block)
587+
->allowElement('p');
588+
589+
$sanitizer = new HtmlSanitizer($config);
590+
self::assertSame('<p>Hello</p>', $sanitizer->sanitize('<foo><div><p><a target="_blank">Hello</a></p></div></foo>'));
591+
}
592+
593+
public function testAllowByDefault()
594+
{
595+
$config = (new HtmlSanitizerConfig())
596+
->defaultAction(HtmlSanitizerAction::Allow)
597+
->allowElement('p')
598+
->dropElement('span');
599+
600+
$sanitizer = new HtmlSanitizer($config);
601+
self::assertSame('<foo><div><p><a>Hello</a></p></div></foo>', $sanitizer->sanitize('<foo data-attr="value"><div class="foo"><p><a target="_blank">Hello<span> World</span></a></p></div></foo>'));
602+
}
581603
}

src/Symfony/Component/HtmlSanitizer/Visitor/DomVisitor.php

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HtmlSanitizer\Visitor;
1313

1414
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
15+
use Symfony\Component\HtmlSanitizer\HtmlSanitizerAction;
1516
use Symfony\Component\HtmlSanitizer\TextSanitizer\StringSanitizer;
1617
use Symfony\Component\HtmlSanitizer\Visitor\AttributeSanitizer\AttributeSanitizerInterface;
1718
use Symfony\Component\HtmlSanitizer\Visitor\Model\Cursor;
@@ -33,6 +34,8 @@
3334
*/
3435
final class DomVisitor
3536
{
37+
private HtmlSanitizerAction $defaultAction = HtmlSanitizerAction::Drop;
38+
3639
/**
3740
* Registry of attributes to forcefully set on nodes, index by element and attribute.
3841
*
@@ -49,11 +52,11 @@ final class DomVisitor
4952
private array $attributeSanitizers = [];
5053

5154
/**
52-
* @param array<string, false|array<string, bool>> $elementsConfig Registry of allowed/blocked elements:
53-
* * If an element is present as a key and contains an array, the element should be allowed
54-
* and the array is the list of allowed attributes.
55-
* * If an element is present as a key and contains "false", the element should be blocked.
56-
* * If an element is not present as a key, the element should be dropped.
55+
* @param array<string, HtmlSanitizerAction|array<string, bool>> $elementsConfig Registry of allowed/blocked elements:
56+
* * If an element is present as a key and contains an array, the element should be allowed
57+
* and the array is the list of allowed attributes.
58+
* * If an element is present as a key and contains an HtmlSanitizerAction, that action applies.
59+
* * If an element is not present as a key, the default action applies.
5760
*/
5861
public function __construct(
5962
private HtmlSanitizerConfig $config,
@@ -68,6 +71,8 @@ public function __construct(
6871
}
6972
}
7073
}
74+
75+
$this->defaultAction = $config->getDefaultAction();
7176
}
7277

7378
public function visit(\DOMDocumentFragment $domNode): ?NodeInterface
@@ -82,32 +87,45 @@ private function visitNode(\DOMNode $domNode, Cursor $cursor): void
8287
{
8388
$nodeName = StringSanitizer::htmlLower($domNode->nodeName);
8489

85-
// Element should be dropped, including its children
86-
if (!\array_key_exists($nodeName, $this->elementsConfig)) {
87-
return;
90+
// Visit recursively if the node was not dropped
91+
if ($this->enterNode($nodeName, $domNode, $cursor)) {
92+
$this->visitChildren($domNode, $cursor);
93+
$cursor->node = $cursor->node->getParent();
8894
}
89-
90-
// Otherwise, visit recursively
91-
$this->enterNode($nodeName, $domNode, $cursor);
92-
$this->visitChildren($domNode, $cursor);
93-
$cursor->node = $cursor->node->getParent();
9495
}
9596

96-
private function enterNode(string $domNodeName, \DOMNode $domNode, Cursor $cursor): void
97+
private function enterNode(string $domNodeName, \DOMNode $domNode, Cursor $cursor): bool
9798
{
99+
if (!\array_key_exists($domNodeName, $this->elementsConfig)) {
100+
$action = $this->defaultAction;
101+
$allowedAttributes = [];
102+
} else {
103+
if (\is_array($this->elementsConfig[$domNodeName])) {
104+
$action = HtmlSanitizerAction::Allow;
105+
$allowedAttributes = $this->elementsConfig[$domNodeName];
106+
} else {
107+
$action = $this->elementsConfig[$domNodeName];
108+
$allowedAttributes = [];
109+
}
110+
}
111+
112+
if ($action === HtmlSanitizerAction::Drop) {
113+
return false;
114+
}
115+
98116
// Element should be blocked, retaining its children
99-
if (false === $this->elementsConfig[$domNodeName]) {
117+
if ($action === HtmlSanitizerAction::Block) {
100118
$node = new BlockedNode($cursor->node);
101119

102120
$cursor->node->addChild($node);
103121
$cursor->node = $node;
104122

105-
return;
123+
return true;
106124
}
107125

108126
// Otherwise create the node
109127
$node = new Node($cursor->node, $domNodeName);
110-
$this->setAttributes($domNodeName, $domNode, $node, $this->elementsConfig[$domNodeName]);
128+
$this->setAttributes($domNodeName, $domNode, $node, $allowedAttributes);
111129

112130
// Force configured attributes
113131
foreach ($this->forcedAttributes[$domNodeName] ?? [] as $attribute => $value) {
@@ -116,6 +134,8 @@ private function enterNode(string $domNodeName, \DOMNode $domNode, Cursor $curso
116134

117135
$cursor->node->addChild($node);
118136
$cursor->node = $node;
137+
138+
return true;
119139
}
120140

121141
private function visitChildren(\DOMNode $domNode, Cursor $cursor): void

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