diff --git a/src/Symfony/Component/HtmlSanitizer/HtmlSanitizerConfig.php b/src/Symfony/Component/HtmlSanitizer/HtmlSanitizerConfig.php
index f7b0b0523bc43..13bb13bdd886a 100644
--- a/src/Symfony/Component/HtmlSanitizer/HtmlSanitizerConfig.php
+++ b/src/Symfony/Component/HtmlSanitizer/HtmlSanitizerConfig.php
@@ -87,6 +87,12 @@ class HtmlSanitizerConfig
*/
private bool $allowRelativeMedias = false;
+ /**
+ * When set to true, the sanitizer will ensure that any element with target="_blank" is also accompanied
+ * by rel="noopener noreferrer" to prevent potential reverse tabnabbing vulnerabilities.
+ */
+ private bool $ensureSafeBlankTarget = true;
+
/**
* Should the URL in the sanitized document be transformed to HTTPS if they are using HTTP.
*/
@@ -257,6 +263,17 @@ public function allowRelativeMedias(bool $allowRelativeMedias = true): static
return $clone;
}
+ /**
+ * Allows the use of the target="_blank" attribute without rel="noopener noreferrer".
+ */
+ public function allowUnsafeBlankTargets(): static
+ {
+ $clone = clone $this;
+ $clone->ensureSafeBlankTarget = false;
+
+ return $clone;
+ }
+
/**
* Transforms URLs using the HTTP scheme to use the HTTPS scheme instead.
*/
@@ -529,6 +546,11 @@ public function getAllowRelativeMedias(): bool
return $this->allowRelativeMedias;
}
+ public function getEnsureSafeBlankTarget(): bool
+ {
+ return $this->ensureSafeBlankTarget;
+ }
+
public function getForceHttpsUrls(): bool
{
return $this->forceHttpsUrls;
diff --git a/src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerAllTest.php b/src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerAllTest.php
index 8699879f67bfd..c5a9b64b0d601 100644
--- a/src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerAllTest.php
+++ b/src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerAllTest.php
@@ -600,4 +600,57 @@ public function testAllowByDefault()
$sanitizer = new HtmlSanitizer($config);
self::assertSame('', $sanitizer->sanitize(''));
}
+
+ /**
+ * @dataProvider provideTargetBlank
+ */
+ public function testSafeTargetBlank(array $allowedAttributes, string $input, string $expectedSanitized)
+ {
+ $sanitizer = new HtmlSanitizer((new HtmlSanitizerConfig())
+ ->allowElement('a', $allowedAttributes)
+ ->allowLinkHosts(['trusted.com'])
+ );
+
+ $sanitized = $sanitizer->sanitize($input);
+
+ $this->assertSame($expectedSanitized, $sanitized);
+ }
+
+ public static function provideTargetBlank()
+ {
+ return [
+ // No rel attribute
+ [
+ ['href', 'target', 'rel'],
+ 'Lorem ipsum',
+ 'Lorem ipsum',
+ ],
+
+ // Normal tags
+ [
+ ['href', 'target', 'rel'],
+ 'Lorem ipsum',
+ 'Lorem ipsum',
+ ],
+
+ // Normal tags
+ [
+ ['href', 'target'],
+ 'Lorem ipsum',
+ 'Lorem ipsum',
+ ],
+ // Missing noreferrer
+ [
+ ['href', 'target', 'rel'],
+ 'Lorem ipsum',
+ 'Lorem ipsum',
+ ],
+ // Missing noopener
+ [
+ ['href', 'target', 'rel'],
+ 'Lorem ipsum',
+ 'Lorem ipsum',
+ ],
+ ];
+ }
}
diff --git a/src/Symfony/Component/HtmlSanitizer/Visitor/DomVisitor.php b/src/Symfony/Component/HtmlSanitizer/Visitor/DomVisitor.php
index e6d34a0967b79..953fd4684c53d 100644
--- a/src/Symfony/Component/HtmlSanitizer/Visitor/DomVisitor.php
+++ b/src/Symfony/Component/HtmlSanitizer/Visitor/DomVisitor.php
@@ -184,5 +184,15 @@ private function setAttributes(string $domNodeName, \DOMNode $domNode, Node $nod
$node->setAttribute($name, $value);
}
}
+ if ('a' === $domNodeName
+ && $this->config->getEnsureSafeBlankTarget()
+ && '_blank' === strtolower($node->getAttribute('target') ?? '')
+ ) { $rel = $node->getAttribute('rel') ?? '';
+ $parts = explode(' ', strtolower($rel));
+ if (!in_array('noopener', $parts, true) || !in_array('noreferrer', $parts, true)) {
+ $parts = array_unique(array_merge($parts, ['noopener', 'noreferrer']));
+ $node->setAttribute('rel', trim(implode(' ', $parts)));
+ }
+ }
}
}
diff --git a/src/Symfony/Component/HtmlSanitizer/Visitor/Node/Node.php b/src/Symfony/Component/HtmlSanitizer/Visitor/Node/Node.php
index d25803776f80d..17f5068146e81 100644
--- a/src/Symfony/Component/HtmlSanitizer/Visitor/Node/Node.php
+++ b/src/Symfony/Component/HtmlSanitizer/Visitor/Node/Node.php
@@ -58,10 +58,7 @@ public function getAttribute(string $name): ?string
public function setAttribute(string $name, ?string $value): void
{
- // Always use only the first declaration (ease sanitization)
- if (!\array_key_exists($name, $this->attributes)) {
- $this->attributes[$name] = $value;
- }
+ $this->attributes[$name] = $value;
}
public function addChild(NodeInterface $node): void