From ff6ad483a2f69d5adb68845b913053e5692ec269 Mon Sep 17 00:00:00 2001
From: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Date: Mon, 18 Dec 2017 19:46:47 +0100
Subject: [PATCH] [FEATURE] Improve creation of URL query strings from arrays

Adds a new method HttpUtility::buildQueryString() using
http_build_query() instead of reimplementing the encoding-process like
the old method GeneralUtility::implodeArrayForUrl() did.

As the parameter $rawurlencodeParamName of implodeArrayForUrl() was set
to "false" by default and used in several places without manually
setting it to "true" using that method could lead to potentially unsafe
non-encoded parameter names.

Some unit-tests had wrong URLs with non-encoded braces [...], which were
adapted to be properly escaped as well.

Resolves: #83334
Releases: master
Change-Id: Ifbaad912f0d658671356dc7bdf1579dacff272df
Reviewed-on: https://review.typo3.org/55079
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
---
 .../NewContentElementController.php           |  3 +-
 .../Controller/EditDocumentController.php     | 11 +--
 .../Controller/LinkBrowserController.php      |  3 +-
 .../backend/Classes/Routing/UriBuilder.php    |  3 +-
 .../Classes/Template/DocumentTemplate.php     |  8 +-
 .../Classes/Template/ModuleTemplate.php       |  5 +-
 .../View/ElementBrowserFolderTreeView.php     | 15 ++--
 .../Tree/View/ElementBrowserPageTreeView.php  |  5 +-
 .../Classes/Utility/BackendUtility.php        |  5 +-
 .../backend/Classes/View/PageLayoutView.php   |  5 +-
 .../core/Classes/Database/QueryView.php       |  3 +-
 .../Classes/TypoScript/TemplateService.php    |  3 +-
 .../core/Classes/Utility/GeneralUtility.php   | 11 +--
 .../core/Classes/Utility/HttpUtility.php      | 32 +++++++
 ...ture-83334-AddImprovedBuildQueryString.rst | 23 +++++
 .../Tests/Unit/Utility/HttpUtilityTest.php    | 86 ++++++++++++++++++-
 .../Classes/Mvc/Web/Routing/UriBuilder.php    |  3 +-
 .../Unit/Mvc/Web/Routing/UriBuilderTest.php   |  4 +-
 .../Controller/FrontendLoginController.php    | 19 ++--
 .../FrontendLoginControllerTest.php           | 57 ++++++++----
 .../ContentObject/ContentObjectRenderer.php   |  5 +-
 .../TypoScriptFrontendController.php          |  6 +-
 .../Middleware/PageArgumentValidator.php      |  5 +-
 .../Classes/Plugin/AbstractPlugin.php         |  3 +-
 .../Classes/Typolink/PageLinkBuilder.php      |  6 +-
 .../TypoScriptFrontendControllerTest.php      |  2 +-
 .../sysext/indexed_search/Classes/Indexer.php |  3 +-
 .../ViewHelpers/Uri/ActionViewHelper.php      | 11 ++-
 .../Classes/Browser/FileBrowser.php           |  3 +-
 .../AbstractLinkBrowserController.php         |  7 +-
 .../RecordList/AbstractDatabaseRecordList.php |  2 +-
 .../Classes/RecordList/DatabaseRecordList.php |  5 +-
 .../Tree/View/ElementBrowserPageTreeView.php  |  5 +-
 .../Tree/View/RecordBrowserPageTreeView.php   |  3 +-
 .../Classes/View/FolderUtilityRenderer.php    | 23 ++---
 .../Classes/Controller/PreviewController.php  |  3 +-
 .../Classes/Preview/PreviewUriBuilder.php     |  3 +-
 37 files changed, 291 insertions(+), 108 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-83334-AddImprovedBuildQueryString.rst

diff --git a/typo3/sysext/backend/Classes/Controller/ContentElement/NewContentElementController.php b/typo3/sysext/backend/Classes/Controller/ContentElement/NewContentElementController.php
index 6d7ed6b7a065..896fb9800eee 100644
--- a/typo3/sysext/backend/Classes/Controller/ContentElement/NewContentElementController.php
+++ b/typo3/sysext/backend/Classes/Controller/ContentElement/NewContentElementController.php
@@ -32,6 +32,7 @@ use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 
 /**
@@ -601,7 +602,7 @@ class NewContentElementController
                         $tempGetVars['defVals']['tt_content']
                     );
                     unset($tempGetVars['defVals']['tt_content']);
-                    $wizardItems[$key]['params'] = GeneralUtility::implodeArrayForUrl('', $tempGetVars);
+                    $wizardItems[$key]['params'] = HttpUtility::buildQueryString($tempGetVars, '&');
                 }
             }
             // If tt_content_defValues are defined...:
diff --git a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
index 70cfcd1c8d7d..1efa5196f3d7 100644
--- a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
+++ b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
@@ -925,10 +925,7 @@ class EditDocumentController
         $this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
         // Set other internal variables:
         $this->R_URL_getvars['returnUrl'] = $this->retUrl;
-        $this->R_URI = $this->R_URL_parts['path'] . '?' . ltrim(GeneralUtility::implodeArrayForUrl(
-            '',
-            $this->R_URL_getvars
-        ), '&');
+        $this->R_URI = $this->R_URL_parts['path'] . HttpUtility::buildQueryString($this->R_URL_getvars, '?');
 
         // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
         $this->MCONF['name'] = 'xMOD_alt_doc.php';
@@ -1056,14 +1053,14 @@ class EditDocumentController
 
         if (!empty($previewConfiguration['useCacheHash'])) {
             $cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
-            $fullLinkParameters = GeneralUtility::implodeArrayForUrl('', array_merge($linkParameters, ['id' => $previewPageId]));
+            $fullLinkParameters = HttpUtility::buildQueryString(array_merge($linkParameters, ['id' => $previewPageId]), '&');
             $cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters);
             $linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters);
         } else {
             $linkParameters['no_cache'] = 1;
         }
 
-        return GeneralUtility::implodeArrayForUrl('', $linkParameters, '', false, true);
+        return HttpUtility::buildQueryString($linkParameters, '&');
     }
 
     /**
@@ -2595,7 +2592,7 @@ class EditDocumentController
             'edit,defVals,overrideVals,columnsOnly,noView,workspace',
             $this->R_URL_getvars
         );
-        $this->storeUrl = GeneralUtility::implodeArrayForUrl('', $this->storeArray);
+        $this->storeUrl = HttpUtility::buildQueryString($this->storeArray, '&');
         $this->storeUrlMd5 = md5($this->storeUrl);
     }
 
diff --git a/typo3/sysext/backend/Classes/Controller/LinkBrowserController.php b/typo3/sysext/backend/Classes/Controller/LinkBrowserController.php
index 3f6da455e683..fe134247c1e8 100644
--- a/typo3/sysext/backend/Classes/Controller/LinkBrowserController.php
+++ b/typo3/sysext/backend/Classes/Controller/LinkBrowserController.php
@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Http\JsonResponse;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
 use TYPO3\CMS\Recordlist\Controller\AbstractLinkBrowserController;
 
@@ -136,7 +137,7 @@ class LinkBrowserController extends AbstractLinkBrowserController
         $formEngineParameters['fieldChangeFunc'] = $this->parameters['fieldChangeFunc'];
         $formEngineParameters['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($this->parameters['fieldChangeFunc']));
 
-        $parameters['data-add-on-params'] .= GeneralUtility::implodeArrayForUrl('P', $formEngineParameters);
+        $parameters['data-add-on-params'] .= HttpUtility::buildQueryString(['P' => $formEngineParameters], '&');
 
         return $parameters;
     }
diff --git a/typo3/sysext/backend/Classes/Routing/UriBuilder.php b/typo3/sysext/backend/Classes/Routing/UriBuilder.php
index 3cacfce07542..2336459118ce 100644
--- a/typo3/sysext/backend/Classes/Routing/UriBuilder.php
+++ b/typo3/sysext/backend/Classes/Routing/UriBuilder.php
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
@@ -159,7 +160,7 @@ class UriBuilder implements SingletonInterface
      */
     protected function buildUri($parameters, $referenceType)
     {
-        $uri = 'index.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', $parameters, '', false, true), '&');
+        $uri = 'index.php' . HttpUtility::buildQueryString($parameters, '?');
         if ($referenceType === self::ABSOLUTE_PATH) {
             $uri = PathUtility::getAbsoluteWebPath(Environment::getBackendPath() . '/' . $uri);
         } else {
diff --git a/typo3/sysext/backend/Classes/Template/DocumentTemplate.php b/typo3/sysext/backend/Classes/Template/DocumentTemplate.php
index dfae8a074081..970e8e228559 100644
--- a/typo3/sysext/backend/Classes/Template/DocumentTemplate.php
+++ b/typo3/sysext/backend/Classes/Template/DocumentTemplate.php
@@ -28,6 +28,7 @@ use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
@@ -399,8 +400,11 @@ function jumpToUrl(URL) {
     public function makeShortcutUrl($gvList, $setList)
     {
         $GET = GeneralUtility::_GET();
-        $storeArray = array_merge(GeneralUtility::compileSelectedGetVarsFromArray($gvList, $GET), ['SET' => GeneralUtility::compileSelectedGetVarsFromArray($setList, (array)$GLOBALS['SOBE']->MOD_SETTINGS)]);
-        return GeneralUtility::implodeArrayForUrl('', $storeArray);
+        $storeArray = array_merge(
+            GeneralUtility::compileSelectedGetVarsFromArray($gvList, $GET),
+            ['SET' => GeneralUtility::compileSelectedGetVarsFromArray($setList, (array)$GLOBALS['SOBE']->MOD_SETTINGS)]
+        );
+        return HttpUtility::buildQueryString($storeArray, '&');
     }
 
     /**
diff --git a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
index d1d565245977..6875734317ba 100644
--- a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
+++ b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Messaging\FlashMessageService;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 
@@ -572,7 +573,7 @@ class ModuleTemplate
      * - SET[] variables a stored in $GLOBALS["SOBE"]->MOD_SETTINGS for backend
      * modules
      *
-     * @return string
+     * @return string GET-parameters for the shortcut-url only(!). String starts with '&'
      * @internal
      */
     public function makeShortcutUrl($gvList, $setList)
@@ -582,7 +583,7 @@ class ModuleTemplate
             GeneralUtility::compileSelectedGetVarsFromArray($gvList, $getParams),
             ['SET' => GeneralUtility::compileSelectedGetVarsFromArray($setList, (array)$GLOBALS['SOBE']->MOD_SETTINGS)]
         );
-        return GeneralUtility::implodeArrayForUrl('', $storeArray);
+        return HttpUtility::buildQueryString($storeArray, '&');
     }
 
     /**
diff --git a/typo3/sysext/backend/Classes/Tree/View/ElementBrowserFolderTreeView.php b/typo3/sysext/backend/Classes/Tree/View/ElementBrowserFolderTreeView.php
index 585d1e23e8d1..792bdf4c2f4b 100644
--- a/typo3/sysext/backend/Classes/Tree/View/ElementBrowserFolderTreeView.php
+++ b/typo3/sysext/backend/Classes/Tree/View/ElementBrowserFolderTreeView.php
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Tree\View;
 
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 
 /**
@@ -63,8 +64,10 @@ class ElementBrowserFolderTreeView extends FolderTreeView
 
         // Wrap icon in link (in ElementBrowser only the "titlelink" is used).
         if ($this->ext_IconMode === 'titlelink') {
-            $parameters = GeneralUtility::implodeArrayForUrl('', $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()]));
-            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim($parameters, '&')) . ');';
+            $parameters = HttpUtility::buildQueryString(
+                $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()])
+            );
+            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters) . ');';
             $theFolderIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
         }
 
@@ -81,8 +84,10 @@ class ElementBrowserFolderTreeView extends FolderTreeView
      */
     public function wrapTitle($title, $folderObject, $bank = 0)
     {
-        $parameters = GeneralUtility::implodeArrayForUrl('', $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()]));
-        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim($parameters, '&'))) . ');">' . $title . '</a>';
+        $parameters = HttpUtility::buildQueryString(
+            $this->linkParameterProvider->getUrlParameters(['identifier' => $folderObject->getCombinedIdentifier()])
+        );
+        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters)) . ');">' . $title . '</a>';
     }
 
     /**
@@ -127,7 +132,7 @@ class ElementBrowserFolderTreeView extends FolderTreeView
         $name = $bMark ? ' name=' . $bMark : '';
         $urlParameters = $this->linkParameterProvider->getUrlParameters([]);
         $urlParameters['PM'] = $cmd;
-        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&')) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
+        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
         return '<a href="#"' . htmlspecialchars($name) . ' onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
     }
 
diff --git a/typo3/sysext/backend/Classes/Tree/View/ElementBrowserPageTreeView.php b/typo3/sysext/backend/Classes/Tree/View/ElementBrowserPageTreeView.php
index ab6780b1f9c3..986528def961 100644
--- a/typo3/sysext/backend/Classes/Tree/View/ElementBrowserPageTreeView.php
+++ b/typo3/sysext/backend/Classes/Tree/View/ElementBrowserPageTreeView.php
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Tree\View;
 
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Page\PageRepository;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 
@@ -118,7 +119,7 @@ class ElementBrowserPageTreeView extends BrowseTreeView
                 $classAttr .= ' active';
             }
             $urlParameters = $this->linkParameterProvider->getUrlParameters(['pid' => (int)$treeItem['row']['uid']]);
-            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&')) . ');';
+            $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ');';
             $cEbullet = $this->ext_isLinkable($treeItem['row']['doktype'], $treeItem['row']['uid'])
                 ? '<a href="#" class="list-tree-show" onclick="' . htmlspecialchars($aOnClick) . '"><i class="fa fa-caret-square-o-right"></i></a>'
                 : '';
@@ -176,7 +177,7 @@ class ElementBrowserPageTreeView extends BrowseTreeView
         $name = $bMark ? ' name=' . $bMark : '';
         $urlParameters = $this->linkParameterProvider->getUrlParameters([]);
         $urlParameters['PM'] = $cmd;
-        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&')) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
+        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($this->getThisScript() . HttpUtility::buildQueryString($urlParameters)) . ',' . GeneralUtility::quoteJSvalue($anchor) . ');';
         return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed')
             . '" href="#"' . htmlspecialchars($name) . ' onclick="' . htmlspecialchars($aOnClick) . '"><i class="fa"></i></a>';
     }
diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
index eeeee6b0775f..cf3c6b29cc2f 100644
--- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php
+++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
@@ -52,6 +52,7 @@ use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
@@ -3091,7 +3092,7 @@ class BackendUtility
      * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=...
      * @param string $addParams Additional parameters to pass to the script.
      * @param string $script The script to send the &id to, if empty it's automatically found
-     * @return string The completes script URL
+     * @return string The complete script URL
      */
     protected static function buildScriptUrl($mainParams, $addParams, $script = '')
     {
@@ -3107,7 +3108,7 @@ class BackendUtility
             $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
             $scriptUrl .= $addParams;
         } else {
-            $scriptUrl = $script . '?' . GeneralUtility::implodeArrayForUrl('', $mainParams) . $addParams;
+            $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams;
         }
 
         return $scriptUrl;
diff --git a/typo3/sysext/backend/Classes/View/PageLayoutView.php b/typo3/sysext/backend/Classes/View/PageLayoutView.php
index 09990441e350..25d90c93f709 100644
--- a/typo3/sysext/backend/Classes/View/PageLayoutView.php
+++ b/typo3/sysext/backend/Classes/View/PageLayoutView.php
@@ -3798,10 +3798,7 @@ class PageLayoutView implements LoggerAwareInterface
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
             $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
-            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(
-                    GeneralUtility::implodeArrayForUrl('', $urlParameters),
-                    '&'
-                );
+            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?');
         }
         return $url;
     }
diff --git a/typo3/sysext/core/Classes/Database/QueryView.php b/typo3/sysext/core/Classes/Database/QueryView.php
index ee176a59647b..2e81f3af84ff 100644
--- a/typo3/sysext/core/Classes/Database/QueryView.php
+++ b/typo3/sysext/core/Classes/Database/QueryView.php
@@ -31,6 +31,7 @@ use TYPO3\CMS\Core\Utility\CsvUtility;
 use TYPO3\CMS\Core\Utility\DebugUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Class used in module tools/dbint (advanced search) and which may hold code specific for that module
@@ -707,7 +708,7 @@ class QueryView
                     ]
                 ],
                 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
-                    . GeneralUtility::implodeArrayForUrl('SET', (array)GeneralUtility::_POST('SET'))
+                    . HttpUtility::buildQueryString(['SET' => (array)GeneralUtility::_POST('SET')], '&')
             ]);
             $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url) . '">'
                 . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
diff --git a/typo3/sysext/core/Classes/TypoScript/TemplateService.php b/typo3/sysext/core/Classes/TypoScript/TemplateService.php
index 7e99e5547051..a1427f04d76e 100644
--- a/typo3/sysext/core/Classes/TypoScript/TemplateService.php
+++ b/typo3/sysext/core/Classes/TypoScript/TemplateService.php
@@ -34,6 +34,7 @@ use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
@@ -1596,7 +1597,7 @@ class TemplateService
         $LD['no_cache'] = $no_cache ? '&no_cache=1' : '';
         // linkVars
         if ($addParams) {
-            $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '', false, true);
+            $LD['linkVars'] = HttpUtility::buildQueryString(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '&');
         } else {
             $LD['linkVars'] = $this->getTypoScriptFrontendController()->linkVars;
         }
diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
index fe4dd8e98750..d1d7576438f6 100644
--- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php
+++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
@@ -2620,12 +2620,11 @@ class GeneralUtility
                 unset($params[$key]);
             }
         }
-        $pString = self::implodeArrayForUrl('', $params);
-        return $pString ? $parts . '?' . ltrim($pString, '&') : $parts;
+        return $parts . HttpUtility::buildQueryString($params, '?');
     }
 
     /**
-     * Takes a full URL, $url, possibly with a querystring and overlays the $getParams arrays values onto the quirystring, packs it all together and returns the URL again.
+     * Takes a full URL, $url, possibly with a querystring and overlays the $getParams arrays values onto the querystring, packs it all together and returns the URL again.
      * So basically it adds the parameters in $getParams to an existing URL, $url
      *
      * @param string $url URL string
@@ -2640,10 +2639,8 @@ class GeneralUtility
             parse_str($parts['query'], $getP);
         }
         ArrayUtility::mergeRecursiveWithOverrule($getP, $getParams);
-        $uP = explode('?', $url);
-        $params = self::implodeArrayForUrl('', $getP);
-        $outurl = $uP[0] . ($params ? '?' . substr($params, 1) : '');
-        return $outurl;
+        [$url] = explode('?', $url);
+        return $url . HttpUtility::buildQueryString($getP, '?');
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Utility/HttpUtility.php b/typo3/sysext/core/Classes/Utility/HttpUtility.php
index 29901e4d4e1b..a137144c5e69 100644
--- a/typo3/sysext/core/Classes/Utility/HttpUtility.php
+++ b/typo3/sysext/core/Classes/Utility/HttpUtility.php
@@ -146,4 +146,36 @@ class HttpUtility
             (isset($urlParts['query']) ? '?' . $urlParts['query'] : '') .
             (isset($urlParts['fragment']) ? '#' . $urlParts['fragment'] : '');
     }
+
+    /**
+     * Implodes a multidimensional array of query parameters to a string of GET parameters (eg. param[key][key2]=value2&param[key][key3]=value3)
+     * and properly encodes parameter names as well as values. Spaces are encoded as %20
+     *
+     * @param array $parameters The (multidimensional) array of query parameters with values
+     * @param string $prependCharacter If the created query string is not empty, prepend this character "?" or "&" else no prepend
+     * @param bool $skipEmptyParameters If true, empty parameters (blank string, empty array, null) are removed.
+     * @return string Imploded result, for example param[key][key2]=value2&param[key][key3]=value3
+     * @see explodeUrl2Array()
+     */
+    public static function buildQueryString(array $parameters, string $prependCharacter = '', bool $skipEmptyParameters = false): string
+    {
+        if (empty($parameters)) {
+            return '';
+        }
+
+        if ($skipEmptyParameters) {
+            // This callback filters empty strings, array and null but keeps zero integers
+            $parameters = ArrayUtility::filterRecursive(
+                $parameters,
+                function ($item) {
+                    return $item !== '' && $item !== [] && $item !== null;
+                }
+            );
+        }
+
+        $queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
+        $prependCharacter = $prependCharacter === '?' || $prependCharacter === '&' ? $prependCharacter : '';
+
+        return $queryString && $prependCharacter ? $prependCharacter . $queryString : $queryString;
+    }
 }
diff --git a/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-83334-AddImprovedBuildQueryString.rst b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-83334-AddImprovedBuildQueryString.rst
new file mode 100644
index 000000000000..d9408a7273b5
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-83334-AddImprovedBuildQueryString.rst
@@ -0,0 +1,23 @@
+.. include:: ../../Includes.txt
+
+========================================================
+Feature: #83334 - Add improved building of query strings
+========================================================
+
+See :issue:`83334`
+
+Description
+===========
+
+The new method :php:`\TYPO3\CMS\Core\Utility\HttpUtility::buildQueryString()` has been added as an enhancement to the `PHP function`_ :php:`http_build_query()`
+to implode multidimensional parameter arrays, properly encode parameter names as well as values with an optional prepend of :php:`?` or :php:`&` if the query
+string is not empty and skipping empty parameters.
+
+.. _`PHP function`: https://secure.php.net/manual/de/function.http-build-query.php
+
+Impact
+======
+
+Parameter arrays can be safely transformed into HTTP GET query strings using the new method.
+
+.. index:: PHP-API, NotScanned
diff --git a/typo3/sysext/core/Tests/Unit/Utility/HttpUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/HttpUtilityTest.php
index 8d9b4f70760e..2e5cc8b377ce 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/HttpUtilityTest.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/HttpUtilityTest.php
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Utility;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -29,7 +30,7 @@ class HttpUtilityTest extends UnitTestCase
      */
     public function isUrlBuiltCorrectly(array $urlParts, $expected)
     {
-        $url = \TYPO3\CMS\Core\Utility\HttpUtility::buildUrl($urlParts);
+        $url = HttpUtility::buildUrl($urlParts);
         $this->assertEquals($expected, $url);
     }
 
@@ -61,4 +62,87 @@ class HttpUtilityTest extends UnitTestCase
             ]
         ];
     }
+
+    /**
+     * Data provider for buildQueryString
+     *
+     * @return array
+     */
+    public function queryStringDataProvider()
+    {
+        $valueArray = ['one' => '√', 'two' => 2];
+
+        return [
+            'Empty input' => ['foo', [], ''],
+            'String parameters' => ['foo', $valueArray, 'foo%5Bone%5D=%E2%88%9A&foo%5Btwo%5D=2'],
+            'Nested array parameters' => ['foo', [$valueArray], 'foo%5B0%5D%5Bone%5D=%E2%88%9A&foo%5B0%5D%5Btwo%5D=2'],
+            'Keep blank parameters' => ['foo', ['one' => '√', ''], 'foo%5Bone%5D=%E2%88%9A&foo%5B0%5D=']
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider queryStringDataProvider
+     * @param string $name
+     * @param array $input
+     * @param string $expected
+     */
+    public function buildQueryStringBuildsValidParameterString($name, array $input, $expected)
+    {
+        if ($name === '') {
+            $this->assertSame($expected, HttpUtility::buildQueryString($input));
+        } else {
+            $this->assertSame($expected, HttpUtility::buildQueryString([$name => $input]));
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringCanSkipEmptyParameters()
+    {
+        $input = ['one' => '√', ''];
+        $expected = 'foo%5Bone%5D=%E2%88%9A';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input], '', true));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringCanUrlEncodeKeyNames()
+    {
+        $input = ['one' => '√', ''];
+        $expected = 'foo%5Bone%5D=%E2%88%9A&foo%5B0%5D=';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input]));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringCanUrlEncodeKeyNamesMultidimensional()
+    {
+        $input = ['one' => ['two' => ['three' => '√']], ''];
+        $expected = 'foo%5Bone%5D%5Btwo%5D%5Bthree%5D=%E2%88%9A&foo%5B0%5D=';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input]));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringSkipsLeadingCharacterOnEmptyParameters()
+    {
+        $input = [];
+        $expected = '';
+        $this->assertSame($expected, HttpUtility::buildQueryString($input, '?', true));
+    }
+
+    /**
+     * @test
+     */
+    public function buildQueryStringSkipsLeadingCharacterOnCleanedEmptyParameters()
+    {
+        $input = ['one' => ''];
+        $expected = '';
+        $this->assertSame($expected, HttpUtility::buildQueryString(['foo' => $input], '?', true));
+    }
 }
diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
index 9eeb3135c9e7..c9c5171511f8 100644
--- a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
+++ b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
@@ -18,6 +18,7 @@ use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
 use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Extbase\Mvc\Request;
 use TYPO3\CMS\Extbase\Mvc\Web\Request as WebRequest;
 
@@ -731,7 +732,7 @@ class UriBuilder
         if (!empty($this->arguments)) {
             $arguments = $this->convertDomainObjectsToIdentityArrays($this->arguments);
             $this->lastArguments = $arguments;
-            $typolinkConfiguration['additionalParams'] = GeneralUtility::implodeArrayForUrl(null, $arguments);
+            $typolinkConfiguration['additionalParams'] = HttpUtility::buildQueryString($arguments, '&');
         }
         if ($this->addQueryString === true) {
             $typolinkConfiguration['addQueryString'] = 1;
diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
index 38a36fcda061..d52c2eea73b5 100644
--- a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
@@ -623,7 +623,7 @@ class UriBuilderTest extends UnitTestCase
     {
         $this->uriBuilder->setTargetPageUid(123);
         $this->uriBuilder->setArguments(['foo' => 'bar', 'baz' => ['extbase' => 'fluid']]);
-        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&foo=bar&baz[extbase]=fluid'];
+        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&foo=bar&baz%5Bextbase%5D=fluid'];
         $actualConfiguration = $this->uriBuilder->_call('buildTypolinkConfiguration');
         $this->assertEquals($expectedConfiguration, $actualConfiguration);
     }
@@ -664,7 +664,7 @@ class UriBuilderTest extends UnitTestCase
         $mockDomainObject2->_set('uid', '321');
         $this->uriBuilder->setTargetPageUid(123);
         $this->uriBuilder->setArguments(['someDomainObject' => $mockDomainObject1, 'baz' => ['someOtherDomainObject' => $mockDomainObject2]]);
-        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&someDomainObject=123&baz[someOtherDomainObject]=321'];
+        $expectedConfiguration = ['parameter' => 123, 'useCacheHash' => 1, 'additionalParams' => '&someDomainObject=123&baz%5BsomeOtherDomainObject%5D=321'];
         $actualConfiguration = $this->uriBuilder->_call('buildTypolinkConfiguration');
         $this->assertEquals($expectedConfiguration, $actualConfiguration);
     }
diff --git a/typo3/sysext/felogin/Classes/Controller/FrontendLoginController.php b/typo3/sysext/felogin/Classes/Controller/FrontendLoginController.php
index b7ac1225a507..5095f3a23b2b 100644
--- a/typo3/sysext/felogin/Classes/Controller/FrontendLoginController.php
+++ b/typo3/sysext/felogin/Classes/Controller/FrontendLoginController.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Plugin\AbstractPlugin;
 
 /**
@@ -907,19 +908,14 @@ class FrontendLoginController extends AbstractPlugin implements LoggerAwareInter
      */
     protected function getPageLink($label, $piVars, $returnUrl = false)
     {
-        $additionalParams = '';
-        if (!empty($piVars)) {
-            foreach ($piVars as $key => $val) {
-                $additionalParams .= '&' . $key . '=' . $val;
-            }
-        }
+        $additionalParams = is_array($piVars) && !empty($piVars) ? $piVars : [];
         // Should GETvars be preserved?
         if ($this->conf['preserveGETvars']) {
-            $additionalParams .= $this->getPreserveGetVars();
+            $additionalParams = array_merge_recursive($additionalParams, $this->getPreserveGetVars());
         }
         $this->conf['linkConfig.']['parameter'] = $this->frontendController->id;
-        if ($additionalParams) {
-            $this->conf['linkConfig.']['additionalParams'] = $additionalParams;
+        if (!empty($additionalParams)) {
+            $this->conf['linkConfig.']['additionalParams'] = HttpUtility::buildQueryString($additionalParams, '&');
         }
         if ($returnUrl) {
             return htmlspecialchars($this->cObj->typoLink_URL($this->conf['linkConfig.']));
@@ -933,7 +929,7 @@ class FrontendLoginController extends AbstractPlugin implements LoggerAwareInter
      * Supports multi-dimensional GET-vars.
      * Some hardcoded values are dropped.
      *
-     * @return string additionalParams-string
+     * @return array additionalParams-array
      */
     protected function getPreserveGetVars()
     {
@@ -954,8 +950,7 @@ class FrontendLoginController extends AbstractPlugin implements LoggerAwareInter
             parse_str(implode('=1&', $preserveQueryStringProperties) . '=1', $preserveQueryParts);
             $preserveQueryParts = \TYPO3\CMS\Core\Utility\ArrayUtility::intersectRecursive($getVars, $preserveQueryParts);
         }
-        $parameters = GeneralUtility::implodeArrayForUrl('', $preserveQueryParts);
-        return $parameters;
+        return $preserveQueryParts;
     }
 
     /**
diff --git a/typo3/sysext/felogin/Tests/Unit/Controller/FrontendLoginControllerTest.php b/typo3/sysext/felogin/Tests/Unit/Controller/FrontendLoginControllerTest.php
index e72dfb7b45cf..48e8e99bb7a9 100644
--- a/typo3/sysext/felogin/Tests/Unit/Controller/FrontendLoginControllerTest.php
+++ b/typo3/sysext/felogin/Tests/Unit/Controller/FrontendLoginControllerTest.php
@@ -322,7 +322,7 @@ class FrontendLoginControllerTest extends UnitTestCase
                     'id' => 42,
                 ],
                 '',
-                '',
+                [],
             ],
             'simple additional parameter is not preserved if not specified in preservedGETvars' => [
                 [
@@ -330,7 +330,7 @@ class FrontendLoginControllerTest extends UnitTestCase
                     'special' => 23,
                 ],
                 '',
-                '',
+                [],
             ],
             'all params except ignored ones are preserved if preservedGETvars is set to "all"' => [
                 [
@@ -344,14 +344,21 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'all',
-                '&special1=23&special2[foo]=bar',
+                [
+                    'special1' => 23,
+                    'special2' => [
+                        'foo' => 'bar',
+                    ],
+                ]
             ],
             'preserve single parameter' => [
                 [
                     'L' => 42,
                 ],
                 'L',
-                '&L=42'
+                [
+                    'L' => 42,
+                ],
             ],
             'preserve whole parameter array' => [
                 [
@@ -364,7 +371,15 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'L,tx_someext',
-                '&L=3&tx_someext[foo]=simple&tx_someext[bar][baz]=simple',
+                [
+                    'L' => 3,
+                    'tx_someext' => [
+                        'foo' => 'simple',
+                        'bar' => [
+                            'baz' => 'simple',
+                        ],
+                    ],
+                ],
             ],
             'preserve part of sub array' => [
                 [
@@ -377,7 +392,14 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'L,tx_someext[bar]',
-                '&L=3&tx_someext[bar][baz]=simple',
+                [
+                    'L' => 3,
+                    'tx_someext' => [
+                        'bar' => [
+                            'baz' => 'simple',
+                        ],
+                    ],
+                ],
             ],
             'preserve keys on different levels' => [
                 [
@@ -394,18 +416,23 @@ class FrontendLoginControllerTest extends UnitTestCase
                     ],
                 ],
                 'L,tx_ext2,tx_ext3[bar]',
-                '&L=3&tx_ext2[foo]=simple&tx_ext3[bar][baz]=simple',
+                [
+                    'L' => 3,
+                    'tx_ext2' => [
+                        'foo' => 'simple',
+                    ],
+                    'tx_ext3' => [
+                        'bar' => [
+                            'baz' => 'simple',
+                        ],
+                    ],
+                ],
             ],
             'preserved value that does not exist in get' => [
                 [],
-                'L,foo[bar]',
-                ''
-            ],
-            'url params are encoded' => [
-                ['tx_ext1' => 'param with spaces and \\ %<>& /'],
-                'L,tx_ext1',
-                '&tx_ext1=param%20with%20spaces%20and%20%5C%20%25%3C%3E%26%20%2F'
-            ],
+                'L,foo%5Bbar%5D',
+                [],
+             ],
         ];
     }
 
diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
index 789f3f78ad59..f0a64d421383 100644
--- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
+++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
@@ -53,6 +53,7 @@ use TYPO3\CMS\Core\TypoScript\TypoScriptService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\DebugUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MailUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
@@ -5564,7 +5565,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
         }
         if (is_array($urlParameters)) {
             if (!empty($urlParameters)) {
-                $conf['additionalParams'] .= GeneralUtility::implodeArrayForUrl('', $urlParameters);
+                $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&');
             }
         } else {
             $conf['additionalParams'] .= $urlParameters;
@@ -5865,7 +5866,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
             $newQueryArray = $currentQueryArray;
         }
         ArrayUtility::mergeRecursiveWithOverrule($newQueryArray, $overruleQueryArguments, $forceOverruleArguments);
-        return GeneralUtility::implodeArrayForUrl('', $newQueryArray, '', false, true);
+        return HttpUtility::buildQueryString($newQueryArray, '&');
     }
 
     /***********************************************
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index 0e29f5dca992..80f49943da37 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -2255,7 +2255,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         if ($this->cHash && is_array($GET)) {
             // Make sure we use the page uid and not the page alias
             $GET['id'] = $this->id;
-            $this->cHash_array = $this->cacheHash->getRelevantParameters(GeneralUtility::implodeArrayForUrl('', $GET));
+            $this->cHash_array = $this->cacheHash->getRelevantParameters(HttpUtility::buildQueryString($GET));
             $cHash_calc = $this->cacheHash->calculateCacheHash($this->cHash_array);
             if (!hash_equals($cHash_calc, $this->cHash)) {
                 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) {
@@ -2271,7 +2271,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             }
         } elseif (is_array($GET)) {
             // No cHash is set, check if that is correct
-            if ($this->cacheHash->doParametersRequireCacheHash(GeneralUtility::implodeArrayForUrl('', $GET))) {
+            if ($this->cacheHash->doParametersRequireCacheHash(HttpUtility::buildQueryString($GET))) {
                 $this->reqCHash();
             }
         }
@@ -3084,7 +3084,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                         // Error: This key must not be an array!
                         continue;
                     }
-                    $value = GeneralUtility::implodeArrayForUrl($parameterName, $value);
+                    $value = HttpUtility::buildQueryString([$parameterName => $value], '&');
                 }
                 $this->linkVars .= $value;
             }
diff --git a/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php b/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php
index 7f0a18c7a6e9..5a9ea04a6ac6 100644
--- a/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php
+++ b/typo3/sysext/frontend/Classes/Middleware/PageArgumentValidator.php
@@ -23,6 +23,7 @@ use Psr\Http\Server\RequestHandlerInterface;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
@@ -102,7 +103,7 @@ class PageArgumentValidator implements MiddlewareInterface
         if ($this->controller->cHash) {
             // Make sure we use the page uid and not the page alias
             $queryParams['id'] = $this->controller->id;
-            $this->controller->cHash_array = $this->cacheHashCalculator->getRelevantParameters(GeneralUtility::implodeArrayForUrl('', $queryParams));
+            $this->controller->cHash_array = $this->cacheHashCalculator->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
             $cHash_calc = $this->cacheHashCalculator->calculateCacheHash($this->controller->cHash_array);
             if (!hash_equals($cHash_calc, $this->controller->cHash)) {
                 // Early return to trigger the error controller
@@ -113,7 +114,7 @@ class PageArgumentValidator implements MiddlewareInterface
                 $this->getTimeTracker()->setTSlogMessage('The incoming cHash "' . $this->controller->cHash . '" and calculated cHash "' . $cHash_calc . '" did not match, so caching was disabled. The fieldlist used was "' . implode(',', array_keys($this->controller->cHash_array)) . '"', 2);
             }
             // No cHash is set, check if that is correct
-        } elseif ($this->cacheHashCalculator->doParametersRequireCacheHash(GeneralUtility::implodeArrayForUrl('', $queryParams))) {
+        } elseif ($this->cacheHashCalculator->doParametersRequireCacheHash(HttpUtility::buildQueryString($queryParams))) {
             // Will disable caching
             $this->controller->reqCHash();
         }
diff --git a/typo3/sysext/frontend/Classes/Plugin/AbstractPlugin.php b/typo3/sysext/frontend/Classes/Plugin/AbstractPlugin.php
index 4b2d1dab75ad..a39bced3ea86 100644
--- a/typo3/sysext/frontend/Classes/Plugin/AbstractPlugin.php
+++ b/typo3/sysext/frontend/Classes/Plugin/AbstractPlugin.php
@@ -24,6 +24,7 @@ use TYPO3\CMS\Core\Localization\LocalizationFactory;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
@@ -384,7 +385,7 @@ class AbstractPlugin
         $conf['useCacheHash'] = $this->pi_USER_INT_obj ? 0 : $cache;
         $conf['no_cache'] = $this->pi_USER_INT_obj ? 0 : !$cache;
         $conf['parameter'] = $altPageId ? $altPageId : ($this->pi_tmpPageId ? $this->pi_tmpPageId : $this->frontendController->id);
-        $conf['additionalParams'] = $this->conf['parent.']['addParams'] . GeneralUtility::implodeArrayForUrl('', $urlParameters, '', true) . $this->pi_moreParams;
+        $conf['additionalParams'] = $this->conf['parent.']['addParams'] . HttpUtility::buildQueryString($urlParameters, '&', true) . $this->pi_moreParams;
         return $this->cObj->typoLink($str, $conf);
     }
 
diff --git a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
index a8d7cafe926d..eae80186307d 100644
--- a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
+++ b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
@@ -31,6 +31,7 @@ use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\RootlineUtility;
 use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
@@ -474,9 +475,8 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         ) {
             $currentQueryArray = [];
             parse_str(GeneralUtility::getIndpEnv('QUERY_STRING'), $currentQueryArray);
-            $currentQueryParams = GeneralUtility::implodeArrayForUrl('', $currentQueryArray, '', false, true);
 
-            if (!trim($currentQueryParams)) {
+            if (empty($currentQueryArray)) {
                 list(, $URLparams) = explode('?', $url);
                 list($URLparams) = explode('#', (string)$URLparams);
                 parse_str($URLparams . $LD['orig_type'], $URLparamsArray);
@@ -753,7 +753,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         $LD['no_cache'] = $no_cache ? '&no_cache=1' : '';
         // linkVars
         if ($addParams) {
-            $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '', false, true);
+            $LD['linkVars'] = HttpUtility::buildQueryString(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '&');
         } else {
             $LD['linkVars'] = $this->getTypoScriptFrontendController()->linkVars;
         }
diff --git a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
index ef4132e8792d..f8f69c55137b 100644
--- a/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Controller/TypoScriptFrontendControllerTest.php
@@ -400,7 +400,7 @@ class TypoScriptFrontendControllerTest extends UnitTestCase
                     'foo' => [ 1, 2, 'f' => [ 4, 5 ] ],
                     'blub' => 123
                 ],
-                '&L=1&foo[0]=1&foo[1]=2&foo[f][0]=4&foo[f][1]=5'
+                '&L=1&foo%5B0%5D=1&foo%5B1%5D=2&foo%5Bf%5D%5B0%5D=4&foo%5Bf%5D%5B1%5D=5'
             ],
             'nested variables' => [
                 'bar|foo(1-2)',
diff --git a/typo3/sysext/indexed_search/Classes/Indexer.php b/typo3/sysext/indexed_search/Classes/Indexer.php
index 374cccb54538..d64183827845 100644
--- a/typo3/sysext/indexed_search/Classes/Indexer.php
+++ b/typo3/sysext/indexed_search/Classes/Indexer.php
@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
@@ -390,7 +391,7 @@ class Indexer
         if ($createCHash) {
             /* @var \TYPO3\CMS\Frontend\Page\CacheHashCalculator $cacheHash */
             $cacheHash = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\CacheHashCalculator::class);
-            $this->conf['cHash'] = $cacheHash->generateForParameters(GeneralUtility::implodeArrayForUrl('', $cHash_array));
+            $this->conf['cHash'] = $cacheHash->generateForParameters(HttpUtility::buildQueryString($cHash_array));
         } else {
             $this->conf['cHash'] = '';
         }
diff --git a/typo3/sysext/install/Classes/ViewHelpers/Uri/ActionViewHelper.php b/typo3/sysext/install/Classes/ViewHelpers/Uri/ActionViewHelper.php
index 512d476e00f3..ea1d44d16a55 100644
--- a/typo3/sysext/install/Classes/ViewHelpers/Uri/ActionViewHelper.php
+++ b/typo3/sysext/install/Classes/ViewHelpers/Uri/ActionViewHelper.php
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Install\ViewHelpers\Uri;
  */
 
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
 use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
@@ -79,9 +80,13 @@ class ActionViewHelper extends AbstractViewHelper
         }
 
         return GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT')
-            . '?'
-            . GeneralUtility::implodeArrayForUrl('install', $arguments)
-            . GeneralUtility::implodeArrayForUrl('', $additionalParams)
+            . HttpUtility::buildQueryString(
+                array_merge(
+                    ['install' => $arguments],
+                    $additionalParams
+                ),
+                '?'
+            )
             . ($section ? '#' . $section : '');
     }
 }
diff --git a/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php b/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php
index 829d61963780..a69c42bddd39 100644
--- a/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php
+++ b/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\ProcessedFile;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 use TYPO3\CMS\Recordlist\View\FolderUtilityRenderer;
@@ -417,7 +418,7 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
             $_MOD_MENU = ['displayThumbs' => ''];
             $_MCONF['name'] = 'file_list';
             $_MOD_SETTINGS = BackendUtility::getModuleData($_MOD_MENU, GeneralUtility::_GP('SET'), $_MCONF['name']);
-            $addParams = GeneralUtility::implodeArrayForUrl('', $this->getUrlParameters(['identifier' => $this->selectedFolder->getCombinedIdentifier()]));
+            $addParams = HttpUtility::buildQueryString($this->getUrlParameters(['identifier' => $this->selectedFolder->getCombinedIdentifier()]), '&');
             $thumbNailCheck = '<div class="checkbox" style="padding:5px 0 15px 0"><label for="checkDisplayThumbs">'
                 . BackendUtility::getFuncCheck(
                     '',
diff --git a/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php b/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php
index 42541bd09fb4..bf90c57e7d34 100644
--- a/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php
+++ b/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php
@@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Http\HtmlResponse;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Recordlist\LinkHandler\LinkHandlerInterface;
 
 /**
@@ -385,8 +386,8 @@ abstract class AbstractLinkBrowserController
             if ($configuration['addParams']) {
                 $addParams = $configuration['addParams'];
             } else {
-                $parameters = GeneralUtility::implodeArrayForUrl('', $this->getUrlParameters(['act' => $identifier]));
-                $addParams = 'onclick="jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue('?' . ltrim($parameters, '&'))) . ');return false;"';
+                $parameters = HttpUtility::buildQueryString($this->getUrlParameters(['act' => $identifier]), '?');
+                $addParams = 'onclick="jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($parameters)) . ');return false;"';
             }
             $menuDef[$identifier] = [
                 'isActive' => $isActive,
@@ -587,7 +588,7 @@ abstract class AbstractLinkBrowserController
         $parameters['params']['allowedExtensions'] = $this->parameters['params']['allowedExtensions'] ?? '';
         $parameters['params']['blindLinkOptions'] = $this->parameters['params']['blindLinkOptions'] ?? '';
         $parameters['params']['blindLinkFields'] = $this->parameters['params']['blindLinkFields'] ?? '';
-        $addPassOnParams = GeneralUtility::implodeArrayForUrl('P', $parameters);
+        $addPassOnParams = HttpUtility::buildQueryString(['P' => $parameters], '&');
 
         $attributes = $this->displayedLinkHandler->getBodyTagAttributes();
         return array_merge(
diff --git a/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
index 8387556381f9..0ca066bd151e 100644
--- a/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
+++ b/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
@@ -1174,7 +1174,7 @@ class AbstractDatabaseRecordList extends AbstractRecordList
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
             $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
-            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
+            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?');
         }
         return $url;
     }
diff --git a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
index 22562a5f8066..9da79bac4d9a 100644
--- a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
+++ b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
@@ -3777,10 +3777,7 @@ class DatabaseRecordList
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
             $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
-            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(
-                    GeneralUtility::implodeArrayForUrl('', $urlParameters),
-                    '&'
-                );
+            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?');
         }
         return $url;
     }
diff --git a/typo3/sysext/recordlist/Classes/Tree/View/ElementBrowserPageTreeView.php b/typo3/sysext/recordlist/Classes/Tree/View/ElementBrowserPageTreeView.php
index 5f20932aa25d..589f0d690a74 100644
--- a/typo3/sysext/recordlist/Classes/Tree/View/ElementBrowserPageTreeView.php
+++ b/typo3/sysext/recordlist/Classes/Tree/View/ElementBrowserPageTreeView.php
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Recordlist\Tree\View;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Extension class for the TBE record browser
@@ -55,7 +56,7 @@ class ElementBrowserPageTreeView extends \TYPO3\CMS\Backend\Tree\View\ElementBro
             return $out;
         }
 
-        $parameters = GeneralUtility::implodeArrayForUrl('', $this->linkParameterProvider->getUrlParameters(['pid' => $v['uid']]));
-        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . ltrim($parameters, '&'))) . ');">' . $title . '</a>';
+        $parameters = HttpUtility::buildQueryString($this->linkParameterProvider->getUrlParameters(['pid' => $v['uid']]));
+        return '<a href="#" onclick="return jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue($this->getThisScript() . $parameters)) . ');">' . $title . '</a>';
     }
 }
diff --git a/typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php b/typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php
index 5a8dded13e5d..01aa108f9c87 100644
--- a/typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php
+++ b/typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Recordlist\Tree\View;
 
 use TYPO3\CMS\Backend\Tree\View\ElementBrowserPageTreeView;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Specific page tree for the record link handler.
@@ -100,7 +101,7 @@ class RecordBrowserPageTreeView extends ElementBrowserPageTreeView
     public function wrapTitle($title, $record, $ext_pArrPages = false)
     {
         $urlParameters = $this->linkParameterProvider->getUrlParameters(['pid' => (int)$record['uid']]);
-        $url = $this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
+        $url = $this->getThisScript() . HttpUtility::buildQueryString($urlParameters);
         $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($url) . ');';
 
         return '<span class="list-tree-title"><a href="#" onclick="' . htmlspecialchars($aOnClick) . '">'
diff --git a/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php b/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php
index a6cf0df89f6d..4b5962bca3b5 100644
--- a/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php
+++ b/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
 
 /**
@@ -85,12 +86,12 @@ class FolderUtilityRenderer
             . htmlspecialchars($folderObject->getCombinedIdentifier()) . '" />';
 
         // Make footer of upload form, including the submit button:
-        $redirectValue = $this->parameterProvider->getScriptUrl() . GeneralUtility::implodeArrayForUrl(
-            '',
-            $this->parameterProvider->getUrlParameters(
-                ['identifier' => $folderObject->getCombinedIdentifier()]
-            )
-        );
+        $redirectValue = $this->parameterProvider->getScriptUrl() . HttpUtility::buildQueryString(
+                $this->parameterProvider->getUrlParameters(
+                    ['identifier' => $folderObject->getCombinedIdentifier()]
+                ),
+                '&'
+            );
         $markup[] = '<input type="hidden" name="data[newfolder][' . $a . '][redirect]" value="' . htmlspecialchars($redirectValue) . '" />';
 
         $markup[] = '</div></form>';
@@ -149,10 +150,10 @@ class FolderUtilityRenderer
                 . htmlspecialchars($combinedIdentifier) . '" />';
             $markup[] = '<input type="hidden" name="data[upload][' . $a . '][data]" value="' . $a . '" />';
         }
-        $redirectValue = $this->parameterProvider->getScriptUrl() . GeneralUtility::implodeArrayForUrl(
-            '',
-            $this->parameterProvider->getUrlParameters(['identifier' => $combinedIdentifier])
-        );
+        $redirectValue = $this->parameterProvider->getScriptUrl() . HttpUtility::buildQueryString(
+                $this->parameterProvider->getUrlParameters(['identifier' => $combinedIdentifier]),
+                '&'
+            );
         $markup[] = '<input type="hidden" name="data[upload][1][redirect]" value="' . htmlspecialchars($redirectValue) . '" />';
 
         if (!empty($fileExtList)) {
@@ -238,7 +239,7 @@ class FolderUtilityRenderer
     public function getFileSearchField($searchWord)
     {
         $action = $this->parameterProvider->getScriptUrl()
-            . GeneralUtility::implodeArrayForUrl('', $this->parameterProvider->getUrlParameters([]));
+            . HttpUtility::buildQueryString($this->parameterProvider->getUrlParameters([]), '&');
 
         $markup = [];
         $markup[] = '<form method="post" action="' . htmlspecialchars($action) . '" style="padding-bottom: 15px;">';
diff --git a/typo3/sysext/workspaces/Classes/Controller/PreviewController.php b/typo3/sysext/workspaces/Classes/Controller/PreviewController.php
index fb75f42f8570..76023275e983 100644
--- a/typo3/sysext/workspaces/Classes/Controller/PreviewController.php
+++ b/typo3/sysext/workspaces/Classes/Controller/PreviewController.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Http\HtmlResponse;
 use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
 use TYPO3\CMS\Workspaces\Service\StagesService;
 use TYPO3\CMS\Workspaces\Service\WorkspaceService;
@@ -121,7 +122,7 @@ class PreviewController
         unset($queryParameters['route'], $queryParameters['token'], $queryParameters['previewWS']);
 
         // Assemble a query string from the retrieved parameters
-        $queryString = GeneralUtility::implodeArrayForUrl('', $queryParameters);
+        $queryString = HttpUtility::buildQueryString($queryParameters, '&');
 
         // fetch the next and previous stage
         $workspaceItemsArray = $this->workspaceService->selectVersionsInWorkspace(
diff --git a/typo3/sysext/workspaces/Classes/Preview/PreviewUriBuilder.php b/typo3/sysext/workspaces/Classes/Preview/PreviewUriBuilder.php
index 38a124239741..324ee34a52e7 100644
--- a/typo3/sysext/workspaces/Classes/Preview/PreviewUriBuilder.php
+++ b/typo3/sysext/workspaces/Classes/Preview/PreviewUriBuilder.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
 use TYPO3\CMS\Workspaces\Service\WorkspaceService;
 
@@ -82,7 +83,7 @@ class PreviewUriBuilder
                 'id' => $uid,
                 'L' => $languageId
             ];
-            return BackendUtility::getViewDomain($uid) . '/index.php?' . GeneralUtility::implodeArrayForUrl('', $linkParams);
+            return BackendUtility::getViewDomain($uid) . '/index.php?' . HttpUtility::buildQueryString($linkParams);
         }
     }
 
-- 
GitLab