diff --git a/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php
index 899c39e08f6d40aa70c809a6f3c61601ed9a7fc3..8caae97e63f175a69951662a8c6386e79141c368 100644
--- a/core/lib/Drupal/Core/Path/AliasStorage.php
+++ b/core/lib/Drupal/Core/Path/AliasStorage.php
@@ -11,9 +11,14 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Database\Query\Condition;
 
 /**
  * Provides a class for CRUD operations on path aliases.
+ *
+ * All queries perform case-insensitive matching on the 'source' and 'alias'
+ * fields, so the aliases '/test-alias' and '/test-Alias' are considered to be
+ * the same, and will both refer to the same internal system path.
  */
 class AliasStorage implements AliasStorageInterface {
   /**
@@ -98,7 +103,13 @@ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NO
   public function load($conditions) {
     $select = $this->connection->select('url_alias');
     foreach ($conditions as $field => $value) {
-      $select->condition($field, $value);
+      if ($field == 'source' || $field == 'alias') {
+        // Use LIKE for case-insensitive matching.
+        $select->condition($field, $this->connection->escapeLike($value), 'LIKE');
+      }
+      else {
+        $select->condition($field, $value);
+      }
     }
     return $select
       ->fields('url_alias')
@@ -115,7 +126,13 @@ public function delete($conditions) {
     $path = $this->load($conditions);
     $query = $this->connection->delete('url_alias');
     foreach ($conditions as $field => $value) {
-      $query->condition($field, $value);
+      if ($field == 'source' || $field == 'alias') {
+        // Use LIKE for case-insensitive matching.
+        $query->condition($field, $this->connection->escapeLike($value), 'LIKE');
+      }
+      else {
+        $query->condition($field, $value);
+      }
     }
     $deleted = $query->execute();
     // @todo Switch to using an event for this instead of a hook.
@@ -128,90 +145,101 @@ public function delete($conditions) {
    * {@inheritdoc}
    */
   public function preloadPathAlias($preloaded, $langcode) {
-    $args = array(
-      ':system[]' => $preloaded,
-      ':langcode' => $langcode,
-      ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-    );
+    $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
+    $select = $this->connection->select('url_alias')
+      ->fields('url_alias', ['source', 'alias']);
+
+    if (!empty($preloaded)) {
+      $conditions = new Condition('OR');
+      foreach ($preloaded as $preloaded_item) {
+        $conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE');
+      }
+      $select->condition($conditions);
+    }
+
     // Always get the language-specific alias before the language-neutral one.
     // For example 'de' is less than 'und' so the order needs to be ASC, while
     // 'xx-lolspeak' is more than 'und' so the order needs to be DESC. We also
     // order by pid ASC so that fetchAllKeyed() returns the most recently
     // created alias for each source. Subsequent queries using fetchField() must
-    // use pid DESC to have the same effect. For performance reasons, the query
-    // builder is not used here.
+    // use pid DESC to have the same effect.
     if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-      // Prevent PDO from complaining about a token the query doesn't use.
-      unset($args[':langcode']);
-      $result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode = :langcode_undetermined ORDER BY pid ASC', $args);
+      array_pop($langcode_list);
     }
     elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-      $result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid ASC', $args);
+      $select->orderBy('langcode', 'ASC');
     }
     else {
-      $result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid ASC', $args);
+      $select->orderBy('langcode', 'DESC');
     }
 
-    return $result->fetchAllKeyed();
+    $select->orderBy('pid', 'ASC');
+    $select->condition('langcode', $langcode_list, 'IN');
+    return $select->execute()->fetchAllKeyed();
   }
 
   /**
    * {@inheritdoc}
    */
   public function lookupPathAlias($path, $langcode) {
-    $args = array(
-      ':source' => $path,
-      ':langcode' => $langcode,
-      ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-    );
-    // See the queries above.
+    $source = $this->connection->escapeLike($path);
+    $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
+
+    // See the queries above. Use LIKE for case-insensitive matching.
+    $select = $this->connection->select('url_alias')
+      ->fields('url_alias', ['alias'])
+      ->condition('source', $source, 'LIKE');
     if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-      unset($args[':langcode']);
-      $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode = :langcode_undetermined ORDER BY pid DESC", $args)->fetchField();
+      array_pop($langcode_list);
     }
     elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-      $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args)->fetchField();
+      $select->orderBy('langcode', 'DESC');
     }
     else {
-      $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args)->fetchField();
+      $select->orderBy('langcode', 'ASC');
     }
 
-    return $alias;
+    $select->orderBy('pid', 'DESC');
+    $select->condition('langcode', $langcode_list, 'IN');
+    return $select->execute()->fetchField();
   }
 
   /**
    * {@inheritdoc}
    */
   public function lookupPathSource($path, $langcode) {
-    $args = array(
-      ':alias' => $path,
-      ':langcode' => $langcode,
-      ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-    );
-    // See the queries above.
+    $alias = $this->connection->escapeLike($path);
+    $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
+
+    // See the queries above. Use LIKE for case-insensitive matching.
+    $select = $this->connection->select('url_alias')
+      ->fields('url_alias', ['source'])
+      ->condition('alias', $alias, 'LIKE');
     if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-      unset($args[':langcode']);
-      $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args);
+      array_pop($langcode_list);
     }
     elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-      $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args);
+      $select->orderBy('langcode', 'DESC');
     }
     else {
-      $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args);
+      $select->orderBy('langcode', 'ASC');
     }
 
-    return $result->fetchField();
+    $select->orderBy('pid', 'DESC');
+    $select->condition('langcode', $langcode_list, 'IN');
+    return $select->execute()->fetchField();
   }
 
   /**
    * {@inheritdoc}
    */
   public function aliasExists($alias, $langcode, $source = NULL) {
+    // Use LIKE and NOT LIKE for case-insensitive matching.
     $query = $this->connection->select('url_alias')
-      ->condition('alias', $alias)
+      ->condition('alias', $this->connection->escapeLike($alias), 'LIKE')
       ->condition('langcode', $langcode);
     if (!empty($source)) {
-      $query->condition('source', $source, '<>');
+      $query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE');
     }
     $query->addExpression('1');
     $query->range(0, 1);
diff --git a/core/lib/Drupal/Core/Path/AliasStorageInterface.php b/core/lib/Drupal/Core/Path/AliasStorageInterface.php
index 5ac77a3efaa19f4f49613d00905009b68ba9734f..3b9c4eecd962e8a932489d771af9016e601c07c4 100644
--- a/core/lib/Drupal/Core/Path/AliasStorageInterface.php
+++ b/core/lib/Drupal/Core/Path/AliasStorageInterface.php
@@ -44,6 +44,9 @@ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NO
   /**
    * Fetches a specific URL alias from the database.
    *
+   * The default implementation performs case-insensitive matching on the
+   * 'source' and 'alias' strings.
+   *
    * @param array $conditions
    *   An array of query conditions.
    *
@@ -60,6 +63,9 @@ public function load($conditions);
   /**
    * Deletes a URL alias.
    *
+   * The default implementation performs case-insensitive matching on the
+   * 'source' and 'alias' strings.
+   *
    * @param array $conditions
    *   An array of criteria.
    */
@@ -82,6 +88,9 @@ public function preloadPathAlias($preloaded, $langcode);
   /**
    * Returns an alias of Drupal system URL.
    *
+   * The default implementation performs case-insensitive matching on the
+   * 'source' and 'alias' strings.
+   *
    * @param string $path
    *   The path to investigate for corresponding path aliases.
    * @param string $langcode
@@ -96,6 +105,9 @@ public function lookupPathAlias($path, $langcode);
   /**
    * Returns Drupal system URL of an alias.
    *
+   * The default implementation performs case-insensitive matching on the
+   * 'source' and 'alias' strings.
+   *
    * @param string $path
    *   The path to investigate for corresponding system URLs.
    * @param string $langcode
@@ -110,6 +122,9 @@ public function lookupPathSource($path, $langcode);
   /**
    * Checks if alias already exists.
    *
+   * The default implementation performs case-insensitive matching on the
+   * 'source' and 'alias' strings.
+   *
    * @param string $alias
    *   Alias to check against.
    * @param string $langcode
@@ -135,8 +150,9 @@ public function languageAliasExists();
    *
    * @param array $header
    *   Table header.
-   * @param string[]|null $keys
-   *   (optional) Search keys.
+   * @param string|null $keys
+   *   (optional) Search keyword that may include one or more '*' as wildcard
+   *   values.
    *
    * @return array
    *   Array of items to be displayed on the current page.
diff --git a/core/modules/path/src/Form/PathFormBase.php b/core/modules/path/src/Form/PathFormBase.php
index 8c84841676092a04ad4b8f387df8f1e18382443c..95d261ac7e8e9b57169b1a273325e7dd881f9a0d 100644
--- a/core/modules/path/src/Form/PathFormBase.php
+++ b/core/modules/path/src/Form/PathFormBase.php
@@ -180,8 +180,22 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     $langcode = $form_state->getValue('langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED);
 
     if ($this->aliasStorage->aliasExists($alias, $langcode, $this->path['source'])) {
-      $form_state->setErrorByName('alias', t('The alias %alias is already in use in this language.', array('%alias' => $alias)));
+      $stored_alias = $this->aliasStorage->load(['alias' => $alias, 'langcode' => $langcode]);
+      if ($stored_alias['alias'] !== $alias) {
+        // The alias already exists with different capitalization as the default
+        // implementation of AliasStorageInterface::aliasExists is
+        // case-insensitive.
+        $form_state->setErrorByName('alias', t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [
+          '%alias' => $alias,
+          '%stored_alias' => $stored_alias['alias'],
+        ]));
+      }
+      else {
+        $form_state->setErrorByName('alias', t('The alias %alias is already in use in this language.', ['%alias' => $alias]));
+      }
     }
+
+
     if (!$this->pathValidator->isValid(trim($source, '/'))) {
       $form_state->setErrorByName('source', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $source)));
     }
diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php
index 651c11f9ea6361b79828ad42f1288b16a8e92fcb..c310f767b370b0eb1acb0adbcc63c07ad713d21e 100644
--- a/core/modules/path/src/Tests/PathAliasTest.php
+++ b/core/modules/path/src/Tests/PathAliasTest.php
@@ -7,7 +7,10 @@
 
 namespace Drupal\path\Tests;
 
+use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Cache\Cache;
+use Drupal\simpletest\RandomGeneratorTrait;
+use Drupal\Core\Database\Database;
 
 /**
  * Add, edit, delete, and change alias and verify its consistency in the
@@ -17,6 +20,8 @@
  */
 class PathAliasTest extends PathTestBase {
 
+  use RandomGeneratorTrait;
+
   /**
    * Modules to enable.
    *
@@ -75,25 +80,45 @@ function testAdminAlias() {
     // Create alias.
     $edit = array();
     $edit['source'] = '/node/' . $node1->id();
-    $edit['alias'] = '/' . $this->randomMachineName(8);
+    $edit['alias'] = '/' . $this->getRandomGenerator()->word(8);
     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
 
     // Confirm that the alias works.
     $this->drupalGet($edit['alias']);
     $this->assertText($node1->label(), 'Alias works.');
     $this->assertResponse(200);
+    // Confirm that the alias works in a case-insensitive way.
+    $this->assertTrue(ctype_lower(ltrim($edit['alias'], '/')));
+    $this->drupalGet($edit['alias']);
+    $this->assertText($node1->label(), 'Alias works lower case.');
+    $this->assertResponse(200);
+    $this->drupalGet(Unicode::strtoupper($edit['alias']));
+    $this->assertText($node1->label(), 'Alias works upper case.');
+    $this->assertResponse(200);
 
     // Change alias to one containing "exotic" characters.
     $pid = $this->getPID($edit['alias']);
 
     $previous = $edit['alias'];
-    $edit['alias'] = "/- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters.
-      "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
-      "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
+    $edit['alias'] = '/alias' . // Lower-case letters.
+      // "Special" ASCII characters.
+      "- ._~!$'\"()*@[]?&+%#,;=:" .
+      // Characters that look like a percent-escaped string.
+      "%23%25%26%2B%2F%3F" .
+      // Characters from various non-ASCII alphabets.
+      "中國書۞";
+    $connection = Database::getConnection();
+    if ($connection->databaseType() != 'sqlite') {
+      // When using LIKE for case-insensitivity, the SQLite driver is
+      // currently unable to find the upper-case versions of non-ASCII
+      // characters.
+      // @todo fix this in https://www.drupal.org/node/2607432
+      $edit['alias'] .= "ïвβéø";
+    }
     $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save'));
 
     // Confirm that the alias works.
-    $this->drupalGet($edit['alias']);
+    $this->drupalGet(Unicode::strtoupper($edit['alias']));
     $this->assertText($node1->label(), 'Changed alias works.');
     $this->assertResponse(200);
 
@@ -114,6 +139,14 @@ function testAdminAlias() {
     // Confirm no duplicate was created.
     $this->assertRaw(t('The alias %alias is already in use in this language.', array('%alias' => $edit['alias'])), 'Attempt to move alias was rejected.');
 
+    $edit_upper = $edit;
+    $edit_upper['alias'] = Unicode::strtoupper($edit['alias']);
+    $this->drupalPostForm('admin/config/search/path/add', $edit_upper, t('Save'));
+    $this->assertRaw(t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [
+      '%alias' => $edit_upper['alias'],
+      '%stored_alias' => $edit['alias'],
+    ]), 'Attempt to move upper-case alias was rejected.');
+
     // Delete alias.
     $this->drupalPostForm('admin/config/search/path/edit/' . $pid, array(), t('Delete'));
     $this->drupalPostForm(NULL, array(), t('Confirm'));
@@ -217,15 +250,27 @@ function testNodeAlias() {
     $elements = $this->xpath("//link[contains(@rel, 'shortlink') and contains(@href, 'node/" . $node1->id() . "')]");
     $this->assertTrue(!empty($elements), 'Page contains shortlink URL.');
 
-    // Change alias to one containing "exotic" characters.
     $previous = $edit['path[0][alias]'];
-    $edit['path[0][alias]'] = "/- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters.
-      "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
-      "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
+    // Change alias to one containing "exotic" characters.
+    $edit['path[0][alias]'] = '/alias' . // Lower-case letters.
+      // "Special" ASCII characters.
+      "- ._~!$'\"()*@[]?&+%#,;=:" .
+      // Characters that look like a percent-escaped string.
+      "%23%25%26%2B%2F%3F" .
+      // Characters from various non-ASCII alphabets.
+      "中國書۞";
+    $connection = Database::getConnection();
+    if ($connection->databaseType() != 'sqlite') {
+      // When using LIKE for case-insensitivity, the SQLite driver is
+      // currently unable to find the upper-case versions of non-ASCII
+      // characters.
+      // @todo fix this in https://www.drupal.org/node/2607432
+      $edit['path[0][alias]'] .= "ïвβéø";
+    }
     $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save'));
 
     // Confirm that the alias works.
-    $this->drupalGet($edit['path[0][alias]']);
+    $this->drupalGet(Unicode::strtoupper($edit['path[0][alias]']));
     $this->assertText($node1->label(), 'Changed alias works.');
     $this->assertResponse(200);
 
diff --git a/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..98180e3a3431919a4d43d5492d53e8035b224601
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\KernelTests\Core\Path\AliasStorageTest.
+ */
+
+namespace Drupal\KernelTests\Core\Path;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Path\AliasStorage
+ * @group path
+ */
+class AliasStorageTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
+  /** @var \Drupal\Core\Path\AliasStorage */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installSchema('system', 'url_alias');
+    $this->storage = $this->container->get('path.alias_storage');
+  }
+
+  /**
+   * @covers ::load
+   */
+  public function testLoad() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+
+    $expected = [
+      'pid' => 1,
+      'alias' => '/test-alias-Case',
+      'source' => '/test-source-Case',
+      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+    ];
+
+    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-Case']));
+    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-case']));
+    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-Case']));
+    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-case']));
+  }
+
+  /**
+   * @covers ::lookupPathAlias
+   */
+  public function testLookupPathAlias() {
+    $this->storage->save('/test-source-Case', '/test-alias');
+
+    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::lookupPathSource
+   */
+  public function testLookupPathSource() {
+    $this->storage->save('/test-source', '/test-alias-Case');
+
+    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::aliasExists
+   */
+  public function testAliasExists() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+
+    $this->assertTrue($this->storage->aliasExists('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertTrue($this->storage->aliasExists('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+}