diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md
index 0898741..e00ebb7 100644
--- a/framework/CHANGELOG.md
+++ b/framework/CHANGELOG.md
@@ -4,6 +4,7 @@ Yii Framework 2 Change Log
 2.0.0-rc under development
 --------------------------
 
+- Bug #2563: Theming is not working if the path map of the theme contains ".." or "." in the paths (qiangxue)
 - Bug #3042: `yii\widgets\Pjax` should end application right after it finishes responding to a pjax request (qiangxue)
 - Bug #3066: `yii\db\mssql\Schema::getTableSchema()` should return null when the table does not exist (qiangxue)
 - Bug #3091: Fixed inconsistent treatment of `Widget::run()` when a widget is used as a container and as a self-contained object (qiangxue)
diff --git a/framework/helpers/BaseFileHelper.php b/framework/helpers/BaseFileHelper.php
index b7fa166..1f4c02c 100644
--- a/framework/helpers/BaseFileHelper.php
+++ b/framework/helpers/BaseFileHelper.php
@@ -28,16 +28,35 @@ class BaseFileHelper
 
     /**
      * Normalizes a file/directory path.
-     * After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`,
-     * and any trailing directory separators will be removed. For example, '/home\demo/' on Linux
-     * will be normalized as '/home/demo'.
+     * The normalization does the following work:
+     *
+     * - Convert all directory separators into `DIRECTORY_SEPARATOR` (e.g. "\a/b\c" becomes "/a/b/c")
+     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
+     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
+     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
+     *
      * @param string $path the file/directory path to be normalized
      * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`.
      * @return string the normalized file/directory path
      */
     public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
     {
-        return rtrim(strtr($path, ['/' => $ds, '\\' => $ds]), $ds);
+        $path = rtrim(strtr($path, ['/' => $ds, '\\' => $ds]), $ds);
+        if (strpos($ds . $path, "{$ds}.") === false && strpos($path, "{$ds}{$ds}") === false) {
+            return $path;
+        }
+        // the path may contain ".", ".." or double slashes, need to clean them up
+        $parts = [];
+        foreach (explode($ds, $path) as $part) {
+            if ($part === '..' && !empty($parts)) {
+                array_pop($parts);
+            } elseif ($part === '.' || $part === '' && !empty($parts)) {
+                continue;
+            } else {
+                $parts[] = $part;
+            }
+        }
+        return implode($ds, $parts);
     }
 
     /**
diff --git a/tests/unit/framework/helpers/FileHelperTest.php b/tests/unit/framework/helpers/FileHelperTest.php
index e0ea416..1ddde26 100644
--- a/tests/unit/framework/helpers/FileHelperTest.php
+++ b/tests/unit/framework/helpers/FileHelperTest.php
@@ -360,7 +360,13 @@ class FileHelperTest extends TestCase
 
     public function testNormalizePath()
     {
-        $this->assertEquals(DIRECTORY_SEPARATOR.'home'.DIRECTORY_SEPARATOR.'demo', FileHelper::normalizePath('/home\demo/'));
+        $ds = DIRECTORY_SEPARATOR;
+        $this->assertEquals("{$ds}a{$ds}b", FileHelper::normalizePath('//a\b/'));
+        $this->assertEquals("{$ds}b{$ds}c", FileHelper::normalizePath('/a/../b/c'));
+        $this->assertEquals("{$ds}c", FileHelper::normalizePath('/a\\b/../..///c'));
+        $this->assertEquals("{$ds}c", FileHelper::normalizePath('/a/.\\b//../../c'));
+        $this->assertEquals("c", FileHelper::normalizePath('/a/.\\b/../..//../c'));
+        $this->assertEquals("..{$ds}c", FileHelper::normalizePath('//a/.\\b//..//..//../../c'));
     }
 
     public function testLocalizedDirectory()