From 5f879e2655c618593c5d1a02331c68fbe599726b Mon Sep 17 00:00:00 2001 From: Tymoteusz Motylewski <t.motylewski@gmail.com> Date: Tue, 1 Sep 2015 21:22:31 +0200 Subject: [PATCH] [!!!][BUGFIX] Indexed Search: Escape search strings for LIKE queries LIKE queries support special placeholders (_ and %). These characters need proper escaping before being used in database queries. Use the escapeStrForLike() method to provide properly escaped strings to the query. This patch handles few places missed by http://review.typo3.org/42806 Resolves: #69227 Releases: master Change-Id: Ie68dd140aafa755be08ba9505b3ac33f00a8f2e2 Reviewed-on: http://review.typo3.org/42969 Reviewed-by: Alexander Opitz <opitz.alexander@googlemail.com> Tested-by: Alexander Opitz <opitz.alexander@googlemail.com> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Benni Mack <benni@typo3.org> --- ...27-StringsForLikeAreNotProperlyEscaped.rst | 34 ++++++ .../Controller/SearchFormController.php | 38 +++--- .../Repository/IndexSearchRepository.php | 42 +++---- .../Classes/Utility/LikeWildcard.php | 59 +++++++++ .../Tests/Unit/Utility/LikeWildcardTest.php | 112 ++++++++++++++++++ 5 files changed, 250 insertions(+), 35 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-69227-StringsForLikeAreNotProperlyEscaped.rst create mode 100644 typo3/sysext/indexed_search/Classes/Utility/LikeWildcard.php create mode 100644 typo3/sysext/indexed_search/Tests/Unit/Utility/LikeWildcardTest.php diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-69227-StringsForLikeAreNotProperlyEscaped.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-69227-StringsForLikeAreNotProperlyEscaped.rst new file mode 100644 index 000000000000..353e5ebf17ab --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-69227-StringsForLikeAreNotProperlyEscaped.rst @@ -0,0 +1,34 @@ +============================================================ +Breaking: #69227 - Strings for like are not properly escaped +============================================================ + +Description +=========== + +The following constants are removed: + +* ``\TYPO3\CMS\IndexedSearch\Controller\SearchFormController::WILDCARD_LEFT`` +* ``\TYPO3\CMS\IndexedSearch\Controller\SearchFormController::WILDCARD_RIGHT`` +* ``\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository::WILDCARD_LEFT`` +* ``\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository::WILDCARD_RIGHT`` + + +Impact +====== + +The mentioned constants don't exist anymore. + + +Affected Installations +====================== + +Third party code using the mentioned constants + + +Migration +========= + +Use the new constants: + +* ``\TYPO3\CMS\IndexedSearch\Utility\LikeWildcard::LEFT`` +* ``\TYPO3\CMS\IndexedSearch\Utility\LikeWildcard::RIGHT`` \ No newline at end of file diff --git a/typo3/sysext/indexed_search/Classes/Controller/SearchFormController.php b/typo3/sysext/indexed_search/Classes/Controller/SearchFormController.php index 4b42ec08d4ba..003c4b8005fe 100755 --- a/typo3/sysext/indexed_search/Classes/Controller/SearchFormController.php +++ b/typo3/sysext/indexed_search/Classes/Controller/SearchFormController.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\IndexedSearch\Controller; use TYPO3\CMS\Core\Html\HtmlParser; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\IndexedSearch\Utility; /** * Index search frontend @@ -169,8 +170,6 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin { */ public $lexerObj; - const WILDCARD_LEFT = 1; - const WILDCARD_RIGHT = 2; /** * Main function, called from TypoScript as a USER_INT object. * @@ -807,15 +806,15 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin { switch ($theType) { case '1': // Part of word - $res = $this->searchWord($sWord, self::WILDCARD_LEFT | self::WILDCARD_RIGHT); + $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH); break; case '2': // First part of word - $res = $this->searchWord($sWord, self::WILDCARD_RIGHT); + $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT); break; case '3': // Last part of word - $res = $this->searchWord($sWord, self::WILDCARD_LEFT); + $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT); break; case '10': // Sounds like @@ -892,13 +891,17 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin { * Search for a word * * @param string $sWord Word to search for - * @param int $mode Bit-field which can contain WILDCARD_LEFT and/or WILDCARD_RIGHT + * @param int $wildcard Bit-field of Utility\LikeWildcard * @return bool|\mysqli_result SQL result pointer */ - public function searchWord($sWord, $mode) { - $wildcard_left = $mode & self::WILDCARD_LEFT ? '%' : ''; - $wildcard_right = $mode & self::WILDCARD_RIGHT ? '%' : ''; - $wSel = 'IW.baseword LIKE \'' . $wildcard_left . $this->databaseConnection->quoteStr($sWord, 'index_words') . $wildcard_right . '\''; + public function searchWord($sWord, $wildcard) { + $likeWildcard = Utility\LikeWildcard::cast($wildcard); + $wSel = $likeWildcard->getLikeQueryPart( + 'index_words', + 'IW.baseword', + $sWord + ); + $this->wSelClauses[] = $wSel; $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0'); return $res; @@ -924,11 +927,18 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin { * @return bool|\mysqli_result SQL result pointer */ public function searchSentence($sSentence) { - $res = $this->databaseConnection->exec_SELECTquery('ISEC.phash', 'index_section ISEC, index_fulltext IFT', 'IFT.fulltextdata LIKE \'%' . $this->databaseConnection->quoteStr($sSentence, 'index_fulltext') . '%\' AND - ISEC.phash = IFT.phash - ' . $this->sectionTableWhere(), 'ISEC.phash'); $this->wSelClauses[] = '1=1'; - return $res; + $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH); + $likePart = $likeWildcard->getLikeQueryPart( + 'index_fulltext', + 'IFT.fulltextdata', + $sSentence + ); + + return $this->databaseConnection->exec_SELECTquery('ISEC.phash', + 'index_section ISEC, index_fulltext IFT', + $likePart . ' AND ISEC.phash = IFT.phash' . $this->sectionTableWhere(), 'ISEC.phash' + ); } /** diff --git a/typo3/sysext/indexed_search/Classes/Domain/Repository/IndexSearchRepository.php b/typo3/sysext/indexed_search/Classes/Domain/Repository/IndexSearchRepository.php index f2a3d7825223..55a7e7d473d1 100644 --- a/typo3/sysext/indexed_search/Classes/Domain/Repository/IndexSearchRepository.php +++ b/typo3/sysext/indexed_search/Classes/Domain/Repository/IndexSearchRepository.php @@ -17,7 +17,7 @@ namespace TYPO3\CMS\IndexedSearch\Domain\Repository; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\IndexedSearch\Indexer; -use TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility; +use TYPO3\CMS\IndexedSearch\Utility; /** * Index search abstraction to search through the index @@ -156,10 +156,6 @@ class IndexSearchRepository { */ protected $displayForbiddenRecords = FALSE; - // constants to help where to use wildcards in SQL like queries - const WILDCARD_LEFT = 1; - const WILDCARD_RIGHT = 2; - /** * initialize all options that are necessary for the search * @@ -345,15 +341,15 @@ class IndexSearchRepository { switch ($theType) { case '1': // Part of word - $res = $this->searchWord($sWord, self::WILDCARD_LEFT | self::WILDCARD_RIGHT); + $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH); break; case '2': // First part of word - $res = $this->searchWord($sWord, self::WILDCARD_RIGHT); + $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT); break; case '3': // Last part of word - $res = $this->searchWord($sWord, self::WILDCARD_LEFT); + $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT); break; case '10': // Sounds like @@ -434,13 +430,16 @@ class IndexSearchRepository { * Search for a word * * @param string $sWord the search word - * @param int $mode constant from this class to see if the wildcard should be left and/or right of the search string + * @param int $wildcard Bit-field of Utility\LikeWildcard * @return bool|\mysqli_result SQL result pointer */ - protected function searchWord($sWord, $mode) { - $wildcard_left = $mode & self::WILDCARD_LEFT ? '%' : ''; - $wildcard_right = $mode & self::WILDCARD_RIGHT ? '%' : ''; - $wSel = 'IW.baseword LIKE \'' . $wildcard_left . $this->getDatabaseConnection()->quoteStr($sWord, 'index_words') . $wildcard_right . '\''; + protected function searchWord($sWord, $wildcard) { + $likeWildcard = Utility\LikeWildcard::cast($wildcard); + $wSel = $likeWildcard->getLikeQueryPart( + 'index_words', + 'IW.baseword', + $sWord + ); $this->wSelClauses[] = $wSel; return $this->execPHashListQuery($wSel, ' AND is_stopword=0'); } @@ -465,16 +464,17 @@ class IndexSearchRepository { */ protected function searchSentence($sWord) { $this->wSelClauses[] = '1=1'; - $sWord = $this->getDatabaseConnection()->quoteStr( - $this->getDatabaseConnection()->escapeStrForLike($sWord, 'index_fulltext'), - 'index_fulltext' + $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH); + $likePart = $likeWildcard->getLikeQueryPart( + 'index_fulltext', + 'IFT.fulltextdata', + $sWord ); + return $this->getDatabaseConnection()->exec_SELECTquery( 'ISEC.phash', 'index_section ISEC, index_fulltext IFT', - 'IFT.fulltextdata LIKE \'%' . $sWord - . '%\' AND ISEC.phash = IFT.phash' - . $this->sectionTableWhere(), + $likePart . ' AND ISEC.phash = IFT.phash' . $this->sectionTableWhere(), 'ISEC.phash' ); } @@ -839,7 +839,7 @@ class IndexSearchRepository { * @return int Integer intepretation of the md5 hash of input string. */ protected function md5inthash($str) { - return IndexedSearchUtility::md5inthash($str); + return Utility\IndexedSearchUtility::md5inthash($str); } /** @@ -851,7 +851,7 @@ class IndexSearchRepository { * @return bool TRUE if given tables are enabled */ protected function isTableUsed($table_list) { - return IndexedSearchUtility::isTableUsed($table_list); + return Utility\IndexedSearchUtility::isTableUsed($table_list); } /** diff --git a/typo3/sysext/indexed_search/Classes/Utility/LikeWildcard.php b/typo3/sysext/indexed_search/Classes/Utility/LikeWildcard.php new file mode 100644 index 000000000000..91af8bf268b8 --- /dev/null +++ b/typo3/sysext/indexed_search/Classes/Utility/LikeWildcard.php @@ -0,0 +1,59 @@ +<?php +namespace TYPO3\CMS\IndexedSearch\Utility; + +/* + * 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! + */ + +/** + * Enumeration object for LikeWildcard + */ +class LikeWildcard extends \TYPO3\CMS\Core\Type\Enumeration { + + const __default = self::BOTH; + + /** @var int Do not use any wildcard */ + const NONE = 0; + + /** @var int Use wildcard on left side */ + const LEFT = 1; + + /** @var int Use wildcard on right side */ + const RIGHT = 2; + + /** @var int Use wildcard on both sides */ + const BOTH = 3; + + /** + * Returns a LIKE clause for sql queries. + * + * @param string $tableName The name of the table to query. + * @param string $fieldName The name of the field to query with LIKE. + * @param string $likeValue The value for the LIKE clause operation. + * @return string + * @throws \TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException + */ + public function getLikeQueryPart($tableName, $fieldName, $likeValue) { + $databaseConnection = $GLOBALS['TYPO3_DB']; + + $likeValue = $databaseConnection->quoteStr( + $databaseConnection->escapeStrForLike($likeValue, $tableName), + $tableName + ); + + return $fieldName . ' LIKE \'' + . ($this->value & self::LEFT ? '%' : '') + . $likeValue + . ($this->value & self::RIGHT ? '%' : '') + . '\''; + } +} diff --git a/typo3/sysext/indexed_search/Tests/Unit/Utility/LikeWildcardTest.php b/typo3/sysext/indexed_search/Tests/Unit/Utility/LikeWildcardTest.php new file mode 100644 index 000000000000..fc01eb569164 --- /dev/null +++ b/typo3/sysext/indexed_search/Tests/Unit/Utility/LikeWildcardTest.php @@ -0,0 +1,112 @@ +<?php +namespace TYPO3\CMS\IndexedSearch\Tests\Unit\Utility; + +/* + * 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\IndexedSearch\Utility\LikeWildcard; + +/** + * This class contains unit tests for the LikeQueryUtility + */ +class LikeWildcardTest extends \TYPO3\CMS\Core\Tests\UnitTestCase { + + /** + * Sets up this test case. + */ + protected function setUp() { + /** @var $databaseConnectionMock \TYPO3\CMS\Core\Database\DatabaseConnection|\PHPUnit_Framework_MockObject_MockObject */ + $databaseConnectionMock = $this->getMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class, array('quoteStr')); + $databaseConnectionMock->method('quoteStr') + ->will($this->returnArgument(0)); + $GLOBALS['TYPO3_DB'] = $databaseConnectionMock; + } + + /** + * @test + * @param string $tableName + * @param string $fieldName + * @param string $likeValue + * @param int $wildcard + * @param string $expected + * @dataProvider getLikeQueryPartDataProvider + */ + public function getLikeQueryPart($tableName, $fieldName, $likeValue, $wildcard, $expected) { + $subject = \TYPO3\CMS\IndexedSearch\Utility\LikeWildcard::cast($wildcard); + $this->assertSame($expected, $subject->getLikeQueryPart($tableName, $fieldName, $likeValue)); + } + + /** + * Returns data sets for the test getLikeQueryPart + * Each dataset is an array with the following elements: + * - the table name + * - the field name + * - the search value + * - the wildcard mode + * - the expected result + * + * @return array + */ + public function getLikeQueryPartDataProvider() { + return [ + 'no placeholders and no wildcard mode' => [ + 'tt_content', + 'body', + 'searchstring', + LikeWildcard::NONE, + 'body LIKE \'searchstring\'' + ], + 'no placeholders and left wildcard mode' => [ + 'tt_content', + 'body', + 'searchstring', + LikeWildcard::LEFT, + 'body LIKE \'%searchstring\'' + ], + 'no placeholders and right wildcard mode' => [ + 'tt_content', + 'body', + 'searchstring', + LikeWildcard::RIGHT, + 'body LIKE \'searchstring%\'' + ], + 'no placeholders and both wildcards mode' => [ + 'tt_content', + 'body', + 'searchstring', + LikeWildcard::BOTH, + 'body LIKE \'%searchstring%\'' + ], + 'underscore placeholder and left wildcard mode' => [ + 'tt_content', + 'body', + 'search_string', + LikeWildcard::LEFT, + 'body LIKE \'%search\\_string\'' + ], + 'percent placeholder and right wildcard mode' => [ + 'tt_content', + 'body', + 'search%string', + LikeWildcard::RIGHT, + 'body LIKE \'search\\%string%\'' + ], + 'percent and underscore placeholder and both wildcards mode' => [ + 'tt_content', + 'body', + '_search%string_', + LikeWildcard::RIGHT, + 'body LIKE \'\\_search\\%string\\_%\'' + ], + ]; + } +} -- GitLab