[FEATURE] Add noopener and noreferrer to external target blank links 94/59194/12
authorDaniel Siepmann <daniel.siepmann@typo3.org>
Tue, 18 Dec 2018 13:16:10 +0000 (14:16 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Fri, 2 Aug 2019 09:52:49 +0000 (11:52 +0200)
All links processed by TypoLink now will add rel="noopener noreferrer"
if necessary.
They are only added for target="_blank" and external hosts.

Resolves: #78488
Releases: master
Change-Id: I24f6a7756e7905ed641e193aff5d1d94375233c0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/59194
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/core/Documentation/Changelog/master/Feature-78488-AddRelnoopenerNoreferrerToExternalLinks.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-78488-AddRelnoopenerNoreferrerToExternalLinks.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-78488-AddRelnoopenerNoreferrerToExternalLinks.rst
new file mode 100644 (file)
index 0000000..6cb6165
--- /dev/null
@@ -0,0 +1,23 @@
+.. include:: ../../Includes.txt
+
+=================================================================
+Feature: #78488 - Add rel="noopener noreferrer" to external links
+=================================================================
+
+See :issue:`78488`
+
+Description
+===========
+
+All links processed by `TypoLink` with external links or using `_blank` have been extended to add `rel="noopener noreferrer"`.
+
+
+Impact
+======
+
+Both properties improve the security of the site:
+
+- `noopener`: This property instructs the browser to open the link without granting the new browsing context access to the document that opened it.
+- `noreferrer`: This property prevents the browser, when navigating to another page, to send the page address, or any other value, as referrer via the Referer HTTP header.
+
+.. index:: Frontend
index 7b92e5d..45b1ab4 100644 (file)
@@ -51,6 +51,7 @@ use TYPO3\CMS\Core\Service\FlexFormService;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
+use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\TypoScript\TypoScriptService;
@@ -5118,6 +5119,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
 
         // Prevent trouble with double and missing spaces between attributes and merge params before implode
         $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
+        $finalTagAttributes = $this->addSecurityRelValues($finalTagAttributes, $target, $tagAttributes['href']);
         $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>';
 
         if (!empty($finalTagParts['aTagParams'])) {
@@ -5166,6 +5168,66 @@ class ContentObjectRenderer implements LoggerAwareInterface
         return $this->wrap($finalAnchorTag . $linkText . '</a>', $wrap);
     }
 
+    protected function addSecurityRelValues(array $tagAttributes, string $target, string $url): array
+    {
+        $relAttribute = 'noopener noreferrer';
+        if ($target !== '_blank' || $this->isInternalUrl($url)) {
+            return $tagAttributes;
+        }
+
+        if (!isset($tagAttributes['rel'])) {
+            $tagAttributes['rel'] = $relAttribute;
+            return $tagAttributes;
+        }
+
+        $tagAttributes['rel'] = implode(' ', array_unique(array_merge(
+            GeneralUtility::trimExplode(' ', $relAttribute),
+            GeneralUtility::trimExplode(' ', $tagAttributes['rel'])
+        )));
+
+        return $tagAttributes;
+    }
+
+    /**
+     * Checks whether the given url is an internal url.
+     *
+     * It will check the host part only, against all configured sites
+     * whether the given host is any. If so, the url is considered internal
+     *
+     * @param string $url The url to check.
+     * @return bool
+     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
+     */
+    protected function isInternalUrl(string $url): bool
+    {
+        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
+        $parsedUrl = parse_url($url);
+        $foundDomains = 0;
+        if (!isset($parsedUrl['host'])) {
+            return true;
+        }
+
+        $cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
+
+        if ($cache->has($cacheIdentifier) === false) {
+            foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
+                if ($site->getBase()->getHost() === $parsedUrl['host']) {
+                    ++$foundDomains;
+                    break;
+                }
+
+                if ($site->getBase()->getHost() === '' && GeneralUtility::isOnCurrentHost($url)) {
+                    ++$foundDomains;
+                    break;
+                }
+            }
+
+            $cache->set($cacheIdentifier, $foundDomains > 0);
+        }
+
+        return (bool)$cache->get($cacheIdentifier);
+    }
+
     /**
      * Based on the input "TypoLink" TypoScript configuration this will return the generated URL
      *
index f2d7ac3..54cdd3d 100644 (file)
@@ -20,8 +20,10 @@ use PHPUnit\Framework\Exception;
 use Prophecy\Argument;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Cache\Backend\NullBackend;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface as CacheFrontendInterface;
+use TYPO3\CMS\Core\Cache\Frontend\NullFrontend;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\UserAspect;
 use TYPO3\CMS\Core\Context\WorkspaceAspect;
@@ -125,6 +127,11 @@ class ContentObjectRendererTest extends UnitTestCase
     ];
 
     /**
+     * @var \Prophecy\Prophecy\ObjectProphecy|CacheManager
+     */
+    protected $cacheManager;
+
+    /**
      * Set up
      */
     protected function setUp(): void
@@ -156,6 +163,9 @@ class ContentObjectRendererTest extends UnitTestCase
         $this->frontendControllerMock->sys_page = $pageRepositoryMock;
         $GLOBALS['TSFE'] = $this->frontendControllerMock;
 
+        $this->cacheManager = $this->prophesize(CacheManager::class);
+        GeneralUtility::setSingletonInstance(CacheManager::class, $this->cacheManager->reveal());
+
         $this->subject = $this->getAccessibleMock(
             ContentObjectRenderer::class,
             ['getResourceFactory', 'getEnvironmentVariable'],
@@ -2716,14 +2726,14 @@ class ContentObjectRendererTest extends UnitTestCase
                     'extTarget' => '_blank',
                     'title' => 'Open new window',
                 ],
-                '<a href="http://typo3.org" title="Open new window" target="_blank" class="url-class">TYPO3</a>',
+                '<a href="http://typo3.org" title="Open new window" target="_blank" class="url-class" rel="noopener noreferrer">TYPO3</a>',
             ],
             'Link to url with attributes in parameter' => [
                 'TYPO3',
                 [
                     'parameter' => 'http://typo3.org _blank url-class "Open new window"',
                 ],
-                '<a href="http://typo3.org" title="Open new window" target="_blank" class="url-class">TYPO3</a>',
+                '<a href="http://typo3.org" title="Open new window" target="_blank" class="url-class" rel="noopener noreferrer">TYPO3</a>',
             ],
             'Link to url with script tag' => [
                 '',
@@ -2792,6 +2802,10 @@ class ContentObjectRendererTest extends UnitTestCase
         ];
         $typoScriptFrontendControllerMockObject->tmpl = $templateServiceObjectMock;
         $GLOBALS['TSFE'] = $typoScriptFrontendControllerMockObject;
+
+        $this->cacheManager->getCache('runtime')->willReturn(new NullBackend(''));
+        $this->cacheManager->getCache('core')->willReturn(new NullFrontend(''));
+
         $this->subject->_set('typoScriptFrontendController', $typoScriptFrontendControllerMockObject);
 
         $this->assertEquals($expectedResult, $this->subject->typoLink($linkText, $configuration));