CVE-2023-41892: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: Craft CMS is a platform for creating digital experiences. This is a high-impact, low-complexity attack vector. Users running Craft installations before 4.4.15 are encouraged to update to at least that version to mitigate the issue. This issue has been fixed in Craft CMS 4.4.15.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 9.8
Published: 2023-09-13T20:15Z
Repository: https://github.com/craftcms/cms
Language: PHP
Files changed: CHANGELOG.md, src/controllers/ConditionsController.php, src/helpers/Component.php, tests/unit/helpers/ComponentHelperTest.php
Commit date: 2023-06-27 16:35:52 -0700
Commit message: Fixed an RCE vulnerability

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8380faeca1c4..cfae390a6dd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,12 +4,14 @@
 
 - Tags fields now only show up to five suggestions. ([#13322](https://github.com/craftcms/cms/issues/13322))
 - The `up`, `migrate/up`, and `migrate/all` commands now revert any project config changes created by migrations on failure.
 - The `up`, `migrate/up`, and `migrate/all` commands now prompt to restore the backup created at the outset of the command, or recommend restoring a backup, on failure.
 - Added `craft\console\controllers\BackupTrait::restore()`.
+- Added `craft\helpers\Component::cleanseConfig()`.
 - Fixed a bug where Single entries weren’t getting preloaded for template macros, if the template body wasn‘t rendered. ([#13312](https://github.com/craftcms/cms/issues/13312))
 - Fixed a bug where asset folders could get dynamically created for elements with temporary slugs. ([#13311](https://github.com/craftcms/cms/issues/13311))
+- Fixed an RCE vulnerability.
 
 ## 4.4.14 - 2023-06-13
 
 - The `utils/fix-field-layout-uids` command now adds missing field layout component UUIDs.
 - The `_includes/forms/date` and `_includes/forms/time` templates now accept a `timeZone` variable.
diff --git a/src/controllers/ConditionsController.php b/src/controllers/ConditionsController.php
index c1be139dc290..ba458375b306 100644
--- a/src/controllers/ConditionsController.php
+++ b/src/controllers/ConditionsController.php
@@ -9,10 +9,11 @@ namespace craft\controllers;
 
 use Craft;
 use craft\base\conditions\ConditionInterface;
 use craft\base\conditions\ConditionRuleInterface;
 use craft\helpers\ArrayHelper;
+use craft\helpers\Component;
 use craft\helpers\Json;
 use craft\web\Controller;
 use Illuminate\Support\Collection;
 
 /**
@@ -39,11 +40,11 @@ class ConditionsController extends Controller
         }
 
         $this->requireCpRequest();
 
         $baseConfig = Json::decodeIfJson($this->request->getBodyParam('config'));
-        $config = $this->request->getBodyParam($baseConfig['name']);
+        $config = Component::cleanseConfig($this->request->getBodyParam($baseConfig['name']));
         $newRuleType = ArrayHelper::remove($config, 'new-rule-type');
         $conditionsService = Craft::$app->getConditions();
         $this->_condition = $conditionsService->createCondition($config);
         Craft::configure($this->_condition, $baseConfig);
 
diff --git a/src/helpers/Component.php b/src/helpers/Component.php
index 8dfa38bc25c7..454535bc0dc6 100644
--- a/src/helpers/Component.php
+++ b/src/helpers/Component.php
@@ -79,10 +79,31 @@ class Component
         }
 
         return true;
     }
 
+    /**
+     * Cleanses a component config of any `on X` or `as X` keys.
+     *
+     * @param array $config
+     * @return array
+     * @since 4.4.15
+     */
+    public static function cleanseConfig(array $config): array
+    {
+        foreach ($config as $key => $value) {
+            if (is_string($key) && (str_starts_with($key, 'on ') || str_starts_with($key, 'as '))) {
+                unset($config[$key]);
+                continue;
+            }
+            if (is_array($value)) {
+                $config[$key] = static::cleanseConfig($value);
+            }
+        }
+        return $config;
+    }
+
     /**
      * Instantiates and populates a component, and ensures that it is an instance of a given interface.
      *
      * @template T of ComponentInterface
      * @param string|array $config The component’s class name, or its config, with a `type` value and optionally a `settings` value.
diff --git a/tests/unit/helpers/ComponentHelperTest.php b/tests/unit/helpers/ComponentHelperTest.php
index b5e53ee61709..ea84c14e5275 100644
--- a/tests/unit/helpers/ComponentHelperTest.php
+++ b/tests/unit/helpers/ComponentHelperTest.php
@@ -113,10 +113,20 @@ class ComponentHelperTest extends TestCase
     public function testIconSvg(string $needle, ?string $icon, string $label): void
     {
         self::assertStringContainsString($needle, Component::iconSvg($icon, $label));
     }
 
+    /**
+     * @dataProvider cleanseConfigDataProvider
+     * @param array $expected
+     * @param array $config
+     */
+    public function testCleanseConfig(array $expected, array $config)
+    {
+        self::assertSame($expected, Component::cleanseConfig($config));
+    }
+
     /**
      * @return array
      */
     public function validateComponentClassDataProvider(): array
     {
@@ -302,6 +312,23 @@ class ComponentHelperTest extends TestCase
             'file-does-not-exist' => ['<title>Default</title>', '/file/does/not/exist.svg', 'Default'],
             'not-an-svg' => ['<title>Default</title>', dirname(__DIR__, 2) . '/_data/assets/files/background.jpeg', 'Default'],
             'aria-hidden' => ['aria-hidden="true"', '<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg>', 'Default'],
         ];
     }
+
+    /**
+     * @return array
+     */
+    public function cleanseConfigDataProvider(): array
+    {
+        return [
+            [
+                ['f' => 'foo', 'b' => 'bar'],
+                ['f' => 'foo', 'b' => 'bar', 'as f' => 'f', 'on b' => 'b'],
+            ],
+            [
+                ['nested' => ['f' => 'foo', 'b' => 'bar']],
+                ['nested' => ['f' => 'foo', 'b' => 'bar', 'as f' => 'f', 'on b' => 'b']],
+            ],
+        ];
+    }
 }

Vulnerable code:         $config = $this->request->getBodyParam($baseConfig['name']);
Fixed code: - Added `craft\helpers\Component::cleanseConfig()`.
- Fixed an RCE vulnerability.
use craft\helpers\Component;
        $config = Component::cleanseConfig($this->request->getBodyParam($baseConfig['name']));
    /**
     * Cleanses a component config of any `on X` or `as X` keys.
     *
     * @param array $config
     * @return array
     * @since 4.4.15
     */
    public static function cleanseConfig(array $config): array
    {
        foreach ($config as $key => $value) {
            if (is_string($key) && (str_starts_with($key, 'on ') || str_starts_with($key, 'as '))) {
                unset($config[$key]);
                continue;
            }
            if (is_array($value)) {
                $config[$key] = static::cleanseConfig($value);
            }
        }
        return $config;
    }

    /**
     * @dataProvider cleanseConfigDataProvider
     * @param array $expected
     * @param array $config
     */
    public function testCleanseConfig(array $expected, array $config)
    {
        self::assertSame($expected, Component::cleanseConfig($config));
    }


    /**
     * @return array
     */
    public function cleanseConfigDataProvider(): array
    {
        return [
            [
                ['f' => 'foo', 'b' => 'bar'],
                ['f' => 'foo', 'b' => 'bar', 'as f' => 'f', 'on b' => 'b'],
            ],
            [
                ['nested' => ['f' => 'foo', 'b' => 'bar']],
                ['nested' => ['f' => 'foo', 'b' => 'bar', 'as f' => 'f', 'on b' => 'b']],
            ],
        ];
    }