[BUGFIX] Fix incomplete validation of source_host field 67/55467/3
authorFrank Naegler <frank.naegler@typo3.org>
Fri, 26 Jan 2018 21:50:52 +0000 (22:50 +0100)
committerAndreas Wolf <andreas.wolf@typo3.org>
Sat, 27 Jan 2018 13:00:45 +0000 (14:00 +0100)
This patch complete the validation of source_host field and
add some tests for the server side validation.
Also a client side validation / manipulation is added.

Resolves: #83659
Releases: master
Change-Id: I5117bd353d116f1366ca779c37ba868fec9f34c8
Reviewed-on: https://review.typo3.org/55467
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Andreas Wolf <andreas.wolf@typo3.org>
Tested-by: Andreas Wolf <andreas.wolf@typo3.org>
typo3/sysext/redirects/Classes/Evaluation/SourceHost.php
typo3/sysext/redirects/Tests/Unit/Evaluation/SourceHostTest.php [new file with mode: 0644]

index 193388a..0b3f25b 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 declare(strict_types = 1);
+
 namespace TYPO3\CMS\Redirects\Evaluation;
 
 /*
@@ -22,6 +23,21 @@ namespace TYPO3\CMS\Redirects\Evaluation;
 class SourceHost
 {
     /**
+     * JavaScript code for client side validation/evaluation
+     *
+     * @return string JavaScript code for client side validation/evaluation
+     */
+    public function returnFieldJS(): string
+    {
+        $jsCode = [];
+        $jsCode[] = 'if (value === \'*\') {return value;}';
+        $jsCode[] = 'var parser = document.createElement(\'a\');';
+        $jsCode[] = 'parser.href = value.indexOf(\'://\') != -1 ? value : \'http://\' + value;';
+        $jsCode[] = 'return parser.host;';
+        return implode(' ', $jsCode);
+    }
+
+    /**
      * Server-side removing of protocol on save
      *
      * @param string $value The field value to be evaluated
@@ -29,6 +45,59 @@ class SourceHost
      */
     public function evaluateFieldValue(string $value): string
     {
-        return preg_replace('#(.*?:\/\/)#', '', $value) ?? '';
+        // 1) Special case: * means any domain
+        if ($value === '*') {
+            return $value;
+        }
+
+        // 2) Check if value contains a protocol like http:// https:// etc...
+        if (strpos($value, '://') !== false) {
+            $tmp = $this->parseUrl($value);
+            if (!empty($tmp)) {
+                return $tmp;
+            }
+        }
+
+        // 3) Check domain name
+        // remove anything after the first "/"
+        $checkValue = $value;
+        if (strpos($value, '/') !== false) {
+            $checkValue = substr($value, 0, strpos($value, '/'));
+        }
+        $validHostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/';
+        if (preg_match_all($validHostnameRegex, $checkValue, $matches, PREG_SET_ORDER) !== false) {
+            if (!empty($matches)) {
+                return $checkValue;
+            }
+        }
+
+        // 4) IPv4 or IPv6
+        $isIP = filter_var($value, FILTER_VALIDATE_IP) === $value;
+        if ($isIP) {
+            return $value;
+        }
+
+        return '';
+    }
+
+    /**
+     * @param string $value
+     * @return string
+     */
+    protected function parseUrl(string $value): string
+    {
+        $urlParts = parse_url($value);
+        if (!empty($urlParts['host'])) {
+            $value = $urlParts['host'];
+
+            // Special case IPv6 with protocol: http://[2001:0db8:85a3:08d3::0370:7344]/
+            // $urlParts['host'] will be [2001:0db8:85a3:08d3::0370:7344]
+            $ipv6Pattern = '/\[([a-zA-Z0-9:]*)\]/';
+            preg_match_all($ipv6Pattern, $urlParts['host'], $ipv6Matches, PREG_SET_ORDER);
+            if (!empty($ipv6Matches[0][1])) {
+                $value = $ipv6Matches[0][1];
+            }
+        }
+        return $value;
     }
 }
diff --git a/typo3/sysext/redirects/Tests/Unit/Evaluation/SourceHostTest.php b/typo3/sysext/redirects/Tests/Unit/Evaluation/SourceHostTest.php
new file mode 100644 (file)
index 0000000..0726227
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Redirects\Tests\Unit\Evaluation;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Redirects\Evaluation\SourceHost;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class SourceHostTest extends UnitTestCase
+{
+    /**
+     * @return array
+     */
+    public function evaluateFieldValueWorksWithDifferentInputsDataProvider(): array
+    {
+        return [
+            // Valid formats
+            'www.domain.com' => ['www.domain.com', 'www.domain.com'],
+            'domain.com' => ['domain.com', 'domain.com'],
+            '127.0.0.1' => ['127.0.0.1', '127.0.0.1'],
+            '2001:0db8:85a3:08d3::0370:7344' => ['2001:0db8:85a3:08d3::0370:7344', '2001:0db8:85a3:08d3::0370:7344'],
+            'http://127.0.0.1' => ['http://127.0.0.1', '127.0.0.1'],
+            'http://www.domain.com' => ['http://www.domain.com', 'www.domain.com'],
+            'https://www.domain.com' => ['https://www.domain.com', 'www.domain.com'],
+            'http://www.domain.com/subfolder/index.php?id=123&foo=bar' => [
+                'http://www.domain.com/subfolder/index.php?id=123&foo=bar',
+                'www.domain.com'
+            ],
+            'https://www.domain.com/subfolder/index.php?id=123&foo=bar' => [
+                'https://www.domain.com/subfolder/index.php?id=123&foo=bar',
+                'www.domain.com'
+            ],
+            'http://www.domain.com/subfolder/' => ['http://www.domain.com/subfolder/', 'www.domain.com'],
+            'https://www.domain.com/subfolder/' => ['https://www.domain.com/subfolder/', 'www.domain.com'],
+            'http://[2001:0db8:85a3:08d3::0370:7344]/' => [
+                'http://[2001:0db8:85a3:08d3::0370:7344]/',
+                '2001:0db8:85a3:08d3::0370:7344'
+            ],
+            'www.domain.com/subfolder/' => ['www.domain.com/subfolder/', 'www.domain.com'],
+            'domain.com/subfolder/' => ['domain.com/subfolder/', 'domain.com'],
+            'www.domain.com/subfolder/index.php?id=123&foo=bar' => [
+                'www.domain.com/subfolder/index.php?id=123&foo=bar',
+                'www.domain.com'
+            ],
+            'domain.com/subfolder/index.php?id=123&foo=bar' => [
+                'domain.com/subfolder/index.php?id=123&foo=bar',
+                'domain.com'
+            ],
+            // special case, * means all domains
+            '*' => ['*', '*'],
+
+            // Invalid formats
+            'mailto:foo@typo3.org' => ['mailto:foo@typo3.org', ''],
+            'mailto:foo@typo3.org?subject=bar' => ['mailto:foo@typo3.org?subject=bar', ''],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider evaluateFieldValueWorksWithDifferentInputsDataProvider
+     * @param string $input
+     * @param string $expected
+     */
+    public function evaluateFieldValueWorksWithDifferentInputs(string $input, string $expected)
+    {
+        $subject = new SourceHost();
+        $this->assertSame($expected, $subject->evaluateFieldValue($input));
+    }
+}