CVE-2021-27903: Missing Authorization (CWE-862)
value: An issue was discovered in Craft CMS before 3.6.7. In some circumstances, a potential Remote Code Execution vulnerability existed on sites that did not restrict administrative changes (if an attacker were somehow able to hijack an administrator's session).
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 9.8
Published: 2021-06-30T12:15Z
Repository: https://github.com/craftcms/cms
Language: PHP
Files changed: CHANGELOG.md, src/volumes/Local.php
Commit date: 2021-02-16 14:04:31 -0800
Commit message: Don’t allow pointing a Local volume to a system directory

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2e428f7ffd..cd785dd79c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,10 +8,13 @@
 ### Changed
 - The `relatedToAssets`, `relatedToCategories`, `relatedToEntries`, `relatedToTags`, and `relatedToUsers` GraphQL arguments now accept a list of multiple criteria sets. ([#7528](https://github.com/craftcms/cms/issues/7528))
 - Adjusted GraphQL complexity values for relational fields.
 - Updated Composer to 2.0.9.
 
+### Security
+- It’s no longer possible to save a Local volume with the File System Path setting set to a system directory (e.g. the `templates/` or `vendor/` folders).
+
 ## 3.6.6 - 2021-02-15
 
 ### Added
 - Added the `successMessageInput()` and `failMessageInput()` Twig functions. ([#7561](https://github.com/craftcms/cms/pull/7561))
 - Added the `prefixGqlRootTypes` config setting, which determines whether the `gqlTypePrefix` config setting should impact `query`, `mutation`, and `subscription` types. ([#7552](https://github.com/craftcms/cms/issues/7552))
diff --git a/src/volumes/Local.php b/src/volumes/Local.php
index a2da21a5668..d50f262927f 100644
--- a/src/volumes/Local.php
+++ b/src/volumes/Local.php
@@ -10,10 +10,11 @@ use craft\errors\VolumeObjectExistsException;
 use craft\errors\VolumeObjectNotFoundException;
 use craft\helpers\FileHelper;
 use League\Flysystem\Adapter\Local as LocalAdapter;
 use League\Flysystem\FileExistsException;
 use League\Flysystem\FileNotFoundException;
+use yii\validators\InlineValidator;
 
 /**
  * The local volume class. Handles the implementation of the local filesystem as a volume in
  * Craft.
  *
@@ -56,13 +57,65 @@ class Local extends FlysystemVolume implements LocalVolumeInterface
      */
     protected function defineRules(): array
     {
         $rules = parent::defineRules();
         $rules[] = [['path'], 'required'];
+        $rules[] = [['path'], 'validatePath'];
         return $rules;
     }
 
+    /**
+     * @param string $attribute
+     * @param array|null $params
+     * @param InlineValidator $validator
+     * @param string $path
+     * @return void
+     * @since 3.6.7
+     */
+    public function validatePath(string $attribute, ?array $params, InlineValidator $validator, string $path): void
+    {
+        if ($created = !file_exists($path)) {
+            FileHelper::createDirectory($path);
+        }
+
+        $path = realpath($this->getRootPath());
+
+        if ($path === false) {
+            return;
+        }
+
+        // Make sure it’s not within any of the system directories
+        $pathService = Craft::$app->getPath();
+        $systemDirs = [
+            Craft::getAlias('@contentMigrations'),
+            Craft::getAlias('@lib'),
+            $pathService->getComposerBackupsPath(false),
+            $pathService->getConfigBackupPath(false),
+            $pathService->getConfigDeltaPath(false),
+            $pathService->getConfigPath(),
+            $pathService->getDbBackupPath(false),
+            $pathService->getLogPath(false),
+            $pathService->getRebrandPath(false),
+            $pathService->getRuntimePath(false),
+            $pathService->getSiteTemplatesPath(),
+            $pathService->getSiteTranslationsPath(),
+            $pathService->getTestsPath(),
+            $pathService->getVendorPath(),
+        ];
+
+        foreach ($systemDirs as $dir) {
+            $dir = realpath($dir);
+            if ($dir !== false && strpos($path . DIRECTORY_SEPARATOR, $dir . DIRECTORY_SEPARATOR) === 0) {
+                $validator->addError($this, $attribute, Craft::t('app', 'Local volumes cannot be located within system directories.'));
+                if ($created) {
+                    FileHelper::removeDirectory($path);
+                }
+                break;
+            }
+        }
+    }
+
     /**
      * @inheritdoc
      */
     public function getSettingsHtml()
     {

Fixed code: ### Security
- It’s no longer possible to save a Local volume with the File System Path setting set to a system directory (e.g. the `templates/` or `vendor/` folders).

use yii\validators\InlineValidator;
        $rules[] = [['path'], 'validatePath'];
    /**
     * @param string $attribute
     * @param array|null $params
     * @param InlineValidator $validator
     * @param string $path
     * @return void
     * @since 3.6.7
     */
    public function validatePath(string $attribute, ?array $params, InlineValidator $validator, string $path): void
    {
        if ($created = !file_exists($path)) {
            FileHelper::createDirectory($path);
        }

        $path = realpath($this->getRootPath());

        if ($path === false) {
            return;
        }

        // Make sure it’s not within any of the system directories
        $pathService = Craft::$app->getPath();
        $systemDirs = [
            Craft::getAlias('@contentMigrations'),
            Craft::getAlias('@lib'),
            $pathService->getComposerBackupsPath(false),
            $pathService->getConfigBackupPath(false),
            $pathService->getConfigDeltaPath(false),
            $pathService->getConfigPath(),
            $pathService->getDbBackupPath(false),
            $pathService->getLogPath(false),
            $pathService->getRebrandPath(false),
            $pathService->getRuntimePath(false),
            $pathService->getSiteTemplatesPath(),
            $pathService->getSiteTranslationsPath(),
            $pathService->getTestsPath(),
            $pathService->getVendorPath(),
        ];

        foreach ($systemDirs as $dir) {
            $dir = realpath($dir);
            if ($dir !== false && strpos($path . DIRECTORY_SEPARATOR, $dir . DIRECTORY_SEPARATOR) === 0) {
                $validator->addError($this, $attribute, Craft::t('app', 'Local volumes cannot be located within system directories.'));
                if ($created) {
                    FileHelper::removeDirectory($path);
                }
                break;
            }
        }
    }