Skip to content

Commit b8617a0

Browse files
[HtmlSanitizer] Honor universal attribute sanitizers, apply maxInputLength to text contexts, document forceAttribute and allowAttribute caveats
1 parent 2e02fc9 commit b8617a0

4 files changed

Lines changed: 54 additions & 6 deletions

File tree

HtmlSanitizer.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public function sanitizeFor(string $element, string $input): string
5151

5252
private function sanitizeWithContext(string $context, string $input): string
5353
{
54+
// Prevent DOS attack induced by extremely long HTML strings
55+
if (-1 !== $this->config->getMaxInputLength() && \strlen($input) > $this->config->getMaxInputLength()) {
56+
$input = substr($input, 0, $this->config->getMaxInputLength());
57+
}
58+
5459
// Text context: early return with HTML encoding
5560
if (W3CReference::CONTEXT_TEXT === $context) {
5661
return StringSanitizer::encodeHtmlEntities($input);
@@ -59,11 +64,6 @@ private function sanitizeWithContext(string $context, string $input): string
5964
// Other context: build a DOM visitor
6065
$this->domVisitors[$context] ??= $this->createDomVisitorForContext($context);
6166

62-
// Prevent DOS attack induced by extremely long HTML strings
63-
if (-1 !== $this->config->getMaxInputLength() && \strlen($input) > $this->config->getMaxInputLength()) {
64-
$input = substr($input, 0, $this->config->getMaxInputLength());
65-
}
66-
6767
// Only operate on valid UTF-8 strings. This is necessary to prevent cross
6868
// site scripting issues on Internet Explorer 6. Idea from Drupal (filter_xss).
6969
if (!$this->isValidUtf8($input)) {

HtmlSanitizerConfig.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,11 @@ public function dropElement(string $element): static
318318
* A list of allowed elements for this attribute can be passed as a second argument.
319319
* Passing "*" will allow all currently allowed elements to use this attribute.
320320
*
321+
* Note: this method is subtractive within the currently allowed elements.
322+
* It restricts the attribute to the listed elements and removes it from any
323+
* other allowed element that previously had it. To add an attribute to one
324+
* element without affecting others, use allowElement($element, [$attribute]).
325+
*
321326
* @param list<string>|string $allowedElements
322327
*/
323328
public function allowAttribute(string $attribute, array|string $allowedElements): static
@@ -370,7 +375,10 @@ public function dropAttribute(string $attribute, array|string $droppedElements):
370375
/**
371376
* Forcefully set the value of a given attribute on a given element.
372377
*
373-
* The attribute will be created on the nodes if it didn't exist.
378+
* The attribute will be created on the nodes if it didn't exist. The
379+
* provided value is written verbatim and is NOT passed through any
380+
* attribute sanitizer (in particular, URL attribute sanitization is
381+
* skipped), so callers are responsible for ensuring the value is safe.
374382
*/
375383
public function forceAttribute(string $element, string $attribute, string $value): static
376384
{

Tests/HtmlSanitizerCustomTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,45 @@ public function sanitizeAttribute(string $element, string $attribute, string $va
531531
);
532532
}
533533

534+
public function testWildcardAttributeSanitizerIsCalled()
535+
{
536+
$config = (new HtmlSanitizerConfig())
537+
->allowElement('div', ['data-foo', 'data-bar'])
538+
->withAttributeSanitizer(new class implements AttributeSanitizerInterface {
539+
public function getSupportedElements(): ?array
540+
{
541+
return null;
542+
}
543+
544+
public function getSupportedAttributes(): ?array
545+
{
546+
return null;
547+
}
548+
549+
public function sanitizeAttribute(string $element, string $attribute, string $value, HtmlSanitizerConfig $config): ?string
550+
{
551+
return strrev($value);
552+
}
553+
})
554+
;
555+
556+
$this->assertSame(
557+
'<div data-foo="cba" data-bar="zyx">Hello world</div>',
558+
$this->sanitize($config, '<div data-foo="abc" data-bar="xyz">Hello world</div>')
559+
);
560+
}
561+
562+
public function testMaxInputLengthIsAppliedToTextContext()
563+
{
564+
$config = (new HtmlSanitizerConfig());
565+
566+
$input = str_repeat('A', $config->getMaxInputLength() + 100);
567+
$expected = str_repeat('A', $config->getMaxInputLength());
568+
569+
$this->assertSame($expected, (new HtmlSanitizer($config))->sanitizeFor('textarea', $input));
570+
$this->assertSame($expected, (new HtmlSanitizer($config))->sanitizeFor('title', $input));
571+
}
572+
534573
private function sanitize(HtmlSanitizerConfig $config, string $input): string
535574
{
536575
return (new HtmlSanitizer($config))->sanitize($input);

Visitor/DomVisitor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ private function setAttributes(string $domNodeName, \DOMNode $domNode, Node $nod
164164
$this->attributeSanitizers[$domNodeName][$name] ?? [],
165165
$this->attributeSanitizers['*'][$name] ?? [],
166166
$this->attributeSanitizers[$domNodeName]['*'] ?? [],
167+
$this->attributeSanitizers['*']['*'] ?? [],
167168
);
168169

169170
foreach ($attributeSanitizers as $sanitizer) {

0 commit comments

Comments
 (0)