diff --git a/core/lib/Drupal/Component/Utility/UrlHelper.php b/core/lib/Drupal/Component/Utility/UrlHelper.php
index 137b4118ce..da9100a664 100644
--- a/core/lib/Drupal/Component/Utility/UrlHelper.php
+++ b/core/lib/Drupal/Component/Utility/UrlHelper.php
@@ -134,6 +134,7 @@ public static function parse($url) {
     $options = [
       'path' => NULL,
       'query' => [],
+      'query_raw' => NULL,
       'fragment' => '',
     ];
 
@@ -160,7 +161,8 @@ public static function parse($url) {
       }
       // If there is a query string, transform it into keyed query parameters.
       if (isset($parts[1])) {
-        parse_str($parts[1], $options['query']);
+        $options['query_raw'] = $parts[1];
+        $options['query'] = static::parseQueryString($parts[1]);
       }
     }
     // Internal URLs.
@@ -171,7 +173,8 @@ public static function parse($url) {
       // Strip the leading slash that was just added.
       $options['path'] = substr($parts['path'], 1);
       if (isset($parts['query'])) {
-        parse_str($parts['query'], $options['query']);
+        $options['query_raw'] = $parts['query'];
+        $options['query'] = static::parseQueryString($parts['query']);
       }
       if (isset($parts['fragment'])) {
         $options['fragment'] = $parts['fragment'];
@@ -181,6 +184,42 @@ public static function parse($url) {
     return $options;
   }
 
+  /**
+   * A wrapper for parse_str() that restores original parameter names.
+   *
+   * The parse_str() function can be used to set variables in the current scope,
+   * although this is deprecated. Because variables in PHP cannot have periods
+   * and spaces in their names, parse_str() converts them to underscores. This
+   * breaks external links and front end libraries that require specific
+   * parameter names. It can also cause name collisions.
+   *
+   * @param string $query
+   *   A query string to parse.
+   *
+   * @return array
+   *   The result of parse_str() with original parameter names restored.
+   */
+  public static function parseQueryString($query) {
+    // Create an array to map parsed parameters to original names.
+    $parsed = [];
+    foreach (explode('&', $query) as $param) {
+      list($name) = explode('=', $param, 2);
+      $name = rawurldecode($name);
+      // Extract the first item of a potentially nested parameter name.
+      $name = strstr($name, '[', TRUE) ?: $name;
+      // Parse the query parameter.
+      parse_str($param, $param);
+      // Merge the parameter into the parsed array.
+      if (isset($parsed[$name]) && is_array($parsed[$name])) {
+        $parsed[$name] = NestedArray::mergeDeepArray([$parsed[$name], reset($param)], TRUE);
+      }
+      else {
+        $parsed[$name] = reset($param);
+      }
+    }
+    return $parsed;
+  }
+
   /**
    * Encodes a Drupal path for use in a URL.
    *
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
index 768103b0c3..d364a04036 100644
--- a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
@@ -74,11 +74,6 @@ protected function buildExternalUrl($uri, array $options = [], $collect_bubbleab
     $parsed = UrlHelper::parse($uri);
     $uri = $parsed['path'];
 
-    $parsed += ['query' => []];
-    $options += ['query' => []];
-
-    $options['query'] = NestedArray::mergeDeepArray([$parsed['query'], $options['query']], TRUE);
-
     if ($parsed['fragment'] && !$options['fragment']) {
       $options['fragment'] = '#' . $parsed['fragment'];
     }
@@ -92,9 +87,15 @@ protected function buildExternalUrl($uri, array $options = [], $collect_bubbleab
       }
     }
     // Append the query.
-    if ($options['query']) {
+    if (!empty($options['query'])) {
+      // Merge the parsed query parameters array with the new parameters
+      $options['query'] = NestedArray::mergeDeepArray([$parsed['query'], $options['query']], TRUE);
       $uri .= '?' . UrlHelper::buildQuery($options['query']);
     }
+    elseif ($parsed['query_raw']) {
+      // Since no new query params are being merged in, use the unprocessed query string for better fidelity.
+      $uri .= '?' . $parsed['query_raw'];
+    }
     // Reassemble.
     $url = $uri . $options['fragment'];
     return $collect_bubbleable_metadata ? (new GeneratedUrl())->setGeneratedUrl($url) : $url;
diff --git a/core/modules/system/tests/src/Functional/Common/UrlTest.php b/core/modules/system/tests/src/Functional/Common/UrlTest.php
index 73a1490e35..53abd5fd01 100644
--- a/core/modules/system/tests/src/Functional/Common/UrlTest.php
+++ b/core/modules/system/tests/src/Functional/Common/UrlTest.php
@@ -265,6 +265,7 @@ public function testDrupalParseUrl() {
           $expected = [
             'path' => $absolute . $script . $path,
             'query' => ['foo' => 'bar', 'bar' => 'baz', 'baz' => ''],
+            'query_raw' => 'foo=bar&bar=baz&baz',
             'fragment' => 'foo',
           ];
           $this->assertEqual($expected, UrlHelper::parse($url), 'URL parsed correctly.');
@@ -277,6 +278,7 @@ public function testDrupalParseUrl() {
     $result = [
       'path' => 'foo/bar:1',
       'query' => [],
+      'query_raw' => NULL,
       'fragment' => '',
     ];
     $this->assertEqual($result, UrlHelper::parse($url), 'Relative URL parsed correctly.');
diff --git a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php
index b44481b348..6e6ab39d45 100644
--- a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php
@@ -256,6 +256,7 @@ public static function providerTestParse() {
         [
           'path' => 'http://www.example.com/my/path',
           'query' => [],
+          'query_raw' => NULL,
           'fragment' => '',
         ],
       ],
@@ -266,14 +267,41 @@ public static function providerTestParse() {
           'query' => [
             'destination' => 'home',
           ],
+          'query_raw' => 'destination=home',
           'fragment' => 'footer',
         ],
       ],
+      'query parameters with characters not allowed in PHP variable names' => [
+        'http://www.example.com/my/path?single.value=&single value=&single_value=&multi.value[first]=&multi.value[second]=&multi value[third]=&multi value[fourth]=&multi_value[fifth]=&multi_value[sixth]=',
+        [
+          'path' => 'http://www.example.com/my/path',
+          'query' => [
+            'single.value' => '',
+            'single value' => '',
+            'single_value' => '',
+            'multi.value' => [
+              'first' => '',
+              'second' => '',
+            ],
+            'multi value' => [
+              'third' => '',
+              'fourth' => '',
+            ],
+            'multi_value' => [
+              'fifth' => '',
+              'sixth' => '',
+            ],
+          ],
+          'query_raw' => 'single.value=&single value=&single_value=&multi.value[first]=&multi.value[second]=&multi value[third]=&multi value[fourth]=&multi_value[fifth]=&multi_value[sixth]=',
+          'fragment' => '',
+        ],
+      ],
       'absolute fragment, no query' => [
         'http://www.example.com/my/path#footer',
         [
           'path' => 'http://www.example.com/my/path',
           'query' => [],
+          'query_raw' => NULL,
           'fragment' => 'footer',
         ],
       ],
@@ -282,6 +310,7 @@ public static function providerTestParse() {
         [
           'path' => '',
           'query' => [],
+          'query_raw' => NULL,
           'fragment' => '',
         ],
       ],
@@ -290,6 +319,7 @@ public static function providerTestParse() {
         [
           'path' => '',
           'query' => [],
+          'query_raw' => NULL,
           'fragment' => '',
         ],
       ],
@@ -300,6 +330,7 @@ public static function providerTestParse() {
           'query' => [
             'destination' => 'home',
           ],
+          'query_raw' => 'destination=home',
           'fragment' => 'footer',
         ],
       ],
@@ -308,6 +339,7 @@ public static function providerTestParse() {
         [
           'path' => '/my/path',
           'query' => [],
+          'query_raw' => NULL,
           'fragment' => 'footer',
         ],
       ],
@@ -319,6 +351,7 @@ public static function providerTestParse() {
             'destination' => 'home',
             'search' => 'http://www.example.com/search?limit=10',
           ],
+          'query_raw' => 'destination=home&search=http://www.example.com/search?limit=10',
           'fragment' => 'footer',
         ],
       ],
@@ -332,6 +365,7 @@ public static function providerTestParse() {
             'referer' => 'http://www.example.com/my/path?destination=home',
             'other' => '',
           ],
+          'query_raw' => 'destination=home&search=http://www.example.com/search?limit=10&referer=http://www.example.com/my/path?destination=home&other',
           'fragment' => 'footer',
         ],
       ],
@@ -344,6 +378,7 @@ public static function providerTestParse() {
             'search' => 'http://www.example.com/search?limit=10',
             'referer' => 'http://www.example.com/my/path?destination=home&other',
           ],
+          'query_raw' => 'destination=home&search=http://www.example.com/search?limit=10&referer=http%3A%2F%2Fwww.example.com%2Fmy%2Fpath%3Fdestination%3Dhome%26other',
           'fragment' => 'footer',
         ],
       ],
diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php
index e52b1049e4..4fb424dfe8 100644
--- a/core/tests/Drupal/Tests/Core/UrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -333,6 +333,16 @@ public function testGetUriForExternalUrl() {
     $this->assertEquals('http://example.com/test', $url->getUri());
   }
 
+  /**
+   * Tests the getUri() method for external URLs with parameters.
+   *
+   * @covers ::getUri
+   */
+  public function testGetUriForExternalUrlWithParameters() {
+    $url = Url::fromUri('http://www.example.com/my/path?single.value=&single value=&single_value=&multi.value[first]=&multi.value[second]=&multi value[third]=&multi value[fourth]=&multi_value[fifth]=&multi_value[sixth]=');
+    $this->assertEquals('http://www.example.com/my/path?single.value=&single value=&single_value=&multi.value[first]=&multi.value[second]=&multi value[third]=&multi value[fourth]=&multi_value[fifth]=&multi_value[sixth]=', $url->getUri());
+  }
+
   /**
    * Tests the getUri() and isExternal() methods for protocol-relative URLs.
    *
diff --git a/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
index 7ef1cc13fd..cf1510de3c 100644
--- a/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
@@ -102,6 +102,7 @@ public function providerTestAssembleWithExternalUrl() {
       'override-deep-query-merge-int-ket' => ['https://example.com/test?120=1', ['query' => ['bar' => ['baz' => 'foo']]], 'https://example.com/test?120=1&bar%5Bbaz%5D=foo'],
       'override-fragment' => ['https://example.com/test?foo=1#bar', ['fragment' => 'baz'], 'https://example.com/test?foo=1#baz'],
       ['//www.drupal.org', [], '//www.drupal.org'],
+      ['https://example.com/test?foo=1&bar=2&foo=3', [], 'https://example.com/test?foo=1&bar=2&foo=3'],
     ];
   }
 
