CVE-2024-35191: Insufficient Information (NVD-CWE-noinfo)
value: Formie is a Craft CMS plugin for creating forms. Prior to 2.1.6, users with access to a form's settings can include malicious Twig code into fields that support Twig. These might be the Submission Title or the Success Message. This code will then be executed upon creating a submission, or rendering the text.  This has been fixed in Formie 2.1.6.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-20T21:15Z
Repository: https://github.com/verbb/formie
Language: PHP
Files changed: src/base/PluginTrait.php, src/base/Webhook.php, src/controllers/FormsController.php, src/controllers/SubmissionsController.php, src/elements/Submission.php, src/fields/formfields/Hidden.php, src/gql/resolvers/mutations/SubmissionResolver.php, src/helpers/Variables.php, src/services/Emails.php, src/services/Rendering.php
Commit date: 2024-03-03 16:01:49 +1100
Commit message: Change all instances of dynamic Twig to use safe, sandboxed environment to protect against potential security issues

Diff: diff --git a/src/base/PluginTrait.php b/src/base/PluginTrait.php
index f43982a8..17d08977 100644
--- a/src/base/PluginTrait.php
+++ b/src/base/PluginTrait.php
@@ -26,10 +26,11 @@ use verbb\formie\services\Submissions;
 use verbb\formie\services\Subscriptions;
 use verbb\formie\services\Syncs;
 use verbb\formie\services\Tokens;
 use verbb\formie\web\assets\forms\FormsAsset;
 use verbb\base\BaseHelper;
+use verbb\base\services\Templates;
 
 use Craft;
 
 use yii\log\Logger;
 
@@ -180,10 +181,15 @@ trait PluginTrait
     public function getSyncs(): Syncs
     {
         return $this->get('syncs');
     }
 
+    public function getTemplates(): Templates
+    {
+        return $this->get('templates');
+    }
+
     public function getTokens(): Tokens
     {
         return $this->get('tokens');
     }
 
@@ -220,10 +226,14 @@ trait PluginTrait
             'statuses' => Statuses::class,
             'stencils' => Stencils::class,
             'submissions' => Submissions::class,
             'subscriptions' => Subscriptions::class,
             'syncs' => Syncs::class,
+            'templates' => [
+                'class' => Templates::class,
+                'pluginClass' => Formie::class,
+            ],
             'tokens' => Tokens::class,
             'vite' => [
                 'class' => VitePluginService::class,
                 'assetClass' => FormsAsset::class,
                 'useDevServer' => true,
diff --git a/src/base/Webhook.php b/src/base/Webhook.php
index eebf39e7..3e5b1a5d 100644
--- a/src/base/Webhook.php
+++ b/src/base/Webhook.php
@@ -1,8 +1,9 @@
 <?php
 namespace verbb\formie\base;
 
+use verbb\formie\Formie;
 use verbb\formie\elements\Submission;
 use verbb\formie\events\ModifyWebhookPayloadEvent;
 
 use Craft;
 use craft\helpers\App;
@@ -85,10 +86,10 @@ abstract class Webhook extends Integration
         return $event->payload;
     }
 
     protected function getWebhookUrl($url, Submission $submission): bool|string|null
     {
-        $url = Craft::$app->getView()->renderObjectTemplate($url, $submission);
+        $url = Formie::$plugin->getTemplates()->renderObjectTemplate($url, $submission);
 
         return App::parseEnv($url);
     }
 }
diff --git a/src/controllers/FormsController.php b/src/controllers/FormsController.php
index 4e33801b..f787d8a6 100644
--- a/src/controllers/FormsController.php
+++ b/src/controllers/FormsController.php
@@ -354,11 +354,11 @@ class FormsController extends Controller
 
         $this->setSuccessFlash(Craft::t('app', 'Form deleted.'));
 
         if ($this->request->getAcceptsJson()) {
             $url = $this->request->getValidatedBodyParam('redirect');
-            $url = Craft::$app->getView()->renderObjectTemplate($url, $form);
+            $url = Formie::$plugin->getTemplates()->renderObjectTemplate($url, $form);
 
             return $this->asJson([
                 'success' => false,
                 'redirect' => UrlHelper::url($url),
             ]);
diff --git a/src/controllers/SubmissionsController.php b/src/controllers/SubmissionsController.php
index aa5fd74d..48bf02ff 100644
--- a/src/controllers/SubmissionsController.php
+++ b/src/controllers/SubmissionsController.php
@@ -691,11 +691,11 @@ class SubmissionsController extends Controller
 
         if (!empty($nextPage)) {
             // Refresh, there's still more pages to complete. Or check if we should "redirect" to a template-defined
             // URL, which is set for every page (commonly the first one, once a submission is available)
             if ($settings->pageRedirectUrl) {
-                $url = $this->getView()->renderObjectTemplate($settings->pageRedirectUrl, $submission);
+                $url = Formie::$plugin->getTemplates()->renderObjectTemplate($settings->pageRedirectUrl, $submission);
 
                 return $this->redirect($url);
             }
 
             return $this->refresh();
@@ -1013,11 +1013,11 @@ class SubmissionsController extends Controller
         // Otherwise, use the form defined
         if (!$redirect) {
             $redirect = $form->getRedirectUrl();
         }
 
-        $redirectUrl = Craft::$app->getView()->renderObjectTemplate($redirect, $submission);
+        $redirectUrl = Formie::$plugin->getTemplates()->renderObjectTemplate($redirect, $submission);
 
         $params = array_merge([
             'success' => $success,
             'submissionId' => $submission->id,
             'currentPageId' => $form->getCurrentPage()->id,
@@ -1187,14 +1187,11 @@ class SubmissionsController extends Controller
         return $nextPage;
     }
 
     private function _setTitle($submission, $form): void
     {
-        // Prevent users using long-hand Twig `{{` to prevent injection execution
-        $submissionTitleFormat = str_replace(['{{', '}}'], ['', ''], $form->settings->submissionTitleFormat);
-
-        $submission->title = Variables::getParsedValue($submissionTitleFormat, $submission, $form);
+        $submission->title = Variables::getParsedValue($form->settings->submissionTitleFormat, $submission, $form);
 
         // Set the default title for the submission, so it can save correctly
         if (!$submission->title) {
             $now = new DateTime('now', new DateTimeZone(Craft::$app->getTimeZone()));
             $submission->title = $now->format('D, d M Y H:i:s');
diff --git a/src/elements/Submission.php b/src/elements/Submission.php
index 92b445db..b3d1e5ac 100644
--- a/src/elements/Submission.php
+++ b/src/elements/Submission.php
@@ -533,14 +533,11 @@ class Submission extends Element
         return UrlHelper::cpUrl($path, $params);
     }
 
     public function updateTitle($form): void
     {
-        // Prevent users using long-hand Twig `{{` to prevent injection execution
-        $submissionTitleFormat = str_replace(['{{', '}}'], ['', ''], $form->settings->submissionTitleFormat);
-
-        if ($customTitle = Variables::getParsedValue($submissionTitleFormat, $this, $form)) {
+        if ($customTitle = Variables::getParsedValue($form->settings->submissionTitleFormat, $this, $form)) {
             $this->title = $customTitle;
 
             // Rather than re-save, directly update the submission record
             Craft::$app->getDb()->createCommand()->update('{{%formie_submissions}}', ['title' => $customTitle], ['id' => $this->id])->execute();
             Craft::$app->getDb()->createCommand()->update('{{%content}}', ['title' => $customTitle], ['elementId' => $this->id])->execute();
diff --git a/src/fields/formfields/Hidden.php b/src/fields/formfields/Hidden.php
index a5adf31f..0e0a72a1 100644
--- a/src/fields/formfields/Hidden.php
+++ b/src/fields/formfields/Hidden.php
@@ -123,12 +123,10 @@ class Hidden extends FormField implements PreviewableFieldInterface
             // Check if there's no value been added on the front-end, and use the default value
             if ($value === '') {
                 $value = $this->defaultValue;
             }
 
-            // Prevent users using long-hand Twig `{{` to prevent injection execution
-            $value = str_replace(['{{', '}}'], ['{', '}'], $value);
             $value = Variables::getParsedValue($value, $element);
 
             // Immediately update the value for the element, so integrations use the up-to-date value
             if ($element) {
                 $element->setFieldValue($this->handle, $value);
diff --git a/src/gql/resolvers/mutations/SubmissionResolver.php b/src/gql/resolvers/mutations/SubmissionResolver.php
index 71f9fa83..8a832178 100644
--- a/src/gql/resolvers/mutations/SubmissionResolver.php
+++ b/src/gql/resolvers/mutations/SubmissionResolver.php
@@ -91,14 +91,11 @@ class SubmissionResolver extends ElementMutationResolver
         }
 
         if (!$submission->title) {
             $settings = $form->settings;
 
-            // Prevent users using long-hand Twig `{{` to prevent injection execution
-            $submissionTitleFormat = str_replace(['{{', '}}'], ['', ''], $settings->submissionTitleFormat);
-
-            $submission->title = Variables::getParsedValue($submissionTitleFormat, $submission, $form);
+            $submission->title = Variables::getParsedValue($settings->submissionTitleFormat, $submission, $form);
 
             if (!$submission->title) {
                 $timeZone = Craft::$app->getTimeZone();
                 $now = new DateTime('now', new DateTimeZone($timeZone));
                 $submission->title = $now->format('Y-m-d H:i');
diff --git a/src/helpers/Variables.php b/src/helpers/Variables.php
index 56b8e8a5..944c1c7a 100644
--- a/src/helpers/Variables.php
+++ b/src/helpers/Variables.php
@@ -261,14 +261,12 @@ class Variables
         Formie::$plugin->getRenderCache()->setFieldVariables($cacheKey, $fieldVariables);
 
         $variables = Formie::$plugin->getRenderCache()->getVariables($cacheKey);
 
         // Try to parse submission + extra variables
-        $view = Craft::$app->getView();
-
         try {
-            return $view->renderObjectTemplate($value, $submission, $variables);
+            return Formie::$plugin->getTemplates()->renderObjectTemplate($value, $submission, $variables);
         } catch (Throwable $e) {
             Formie::error(Craft::t('formie', 'Failed to render dynamic string “{value}”. Template error: “{message}” {file}:{line}', [
                 'value' => $originalValue,
                 'message' => $e->getMessage(),
                 'file' => $e->getFile(),
diff --git a/src/services/Emails.php b/src/services/Emails.php
index 12c7237f..231da92b 100644
--- a/src/services/Emails.php
+++ b/src/services/Emails.php
@@ -656,11 +656,11 @@ class Emails extends Component
             'notification' => $notification,
         ];
 
         // Generate the filename correctly.
         $filenameFormat = $template->filenameFormat ?? 'Submission-{submission.id}';
-        $fileName = Craft::$app->getView()->renderObjectTemplate($filenameFormat, $variables);
+        $fileName = Formie::$plugin->getTemplates()->renderObjectTemplate($filenameFormat, $variables);
 
         $message->attach($pdfPath, ['fileName' => $fileName . '.pdf', 'contentType' => 'application/pdf']);
 
         // Store for later
         $this->_tempAttachments[] = $pdfPath;
diff --git a/src/services/Rendering.php b/src/services/Rendering.php
index 26028325..22e87a47 100644
--- a/src/services/Rendering.php
+++ b/src/services/Rendering.php
@@ -485,13 +485,13 @@ class Rendering extends Component
         // Try to populate fields with their default value
         foreach ($values as $key => $value) {
             try {
                 $field = $form->getFieldByHandle($key);
 
-                // Prevent users using long-hand Twig `{{` to prevent injection execution. Only an issue for 
+                // Ensure that we protect against developers using Twig to populate values from query strings
                 if (is_string($value)) {
-                    $value = str_replace(['{{', '}}', '{%', '%}'], ['{', '}', '', ''], $value);
+                    $value = Formie::$plugin->getTemplates()->renderString($value);
                 }
 
                 if ($field) {
                     // Store any visibly disabled fields against the form to apply later
                     if ($field->visibility === 'disabled') {

Vulnerable code:         $url = Craft::$app->getView()->renderObjectTemplate($url, $submission);
            $url = Craft::$app->getView()->renderObjectTemplate($url, $form);
                $url = $this->getView()->renderObjectTemplate($settings->pageRedirectUrl, $submission);
        $redirectUrl = Craft::$app->getView()->renderObjectTemplate($redirect, $submission);
        // Prevent users using long-hand Twig `{{` to prevent injection execution
        $submissionTitleFormat = str_replace(['{{', '}}'], ['', ''], $form->settings->submissionTitleFormat);

        $submission->title = Variables::getParsedValue($submissionTitleFormat, $submission, $form);
        // Prevent users using long-hand Twig `{{` to prevent injection execution
        $submissionTitleFormat = str_replace(['{{', '}}'], ['', ''], $form->settings->submissionTitleFormat);

        if ($customTitle = Variables::getParsedValue($submissionTitleFormat, $this, $form)) {
            // Prevent users using long-hand Twig `{{` to prevent injection execution
            $value = str_replace(['{{', '}}'], ['{', '}'], $value);
            // Prevent users using long-hand Twig `{{` to prevent injection execution
            $submissionTitleFormat = str_replace(['{{', '}}'], ['', ''], $settings->submissionTitleFormat);

            $submission->title = Variables::getParsedValue($submissionTitleFormat, $submission, $form);
        $view = Craft::$app->getView();

            return $view->renderObjectTemplate($value, $submission, $variables);
        $fileName = Craft::$app->getView()->renderObjectTemplate($filenameFormat, $variables);
                // Prevent users using long-hand Twig `{{` to prevent injection execution. Only an issue for 
                    $value = str_replace(['{{', '}}', '{%', '%}'], ['{', '}', '', ''], $value);
Fixed code: use verbb\base\services\Templates;
    public function getTemplates(): Templates
    {
        return $this->get('templates');
    }

            'templates' => [
                'class' => Templates::class,
                'pluginClass' => Formie::class,
            ],
use verbb\formie\Formie;
        $url = Formie::$plugin->getTemplates()->renderObjectTemplate($url, $submission);
            $url = Formie::$plugin->getTemplates()->renderObjectTemplate($url, $form);
                $url = Formie::$plugin->getTemplates()->renderObjectTemplate($settings->pageRedirectUrl, $submission);
        $redirectUrl = Formie::$plugin->getTemplates()->renderObjectTemplate($redirect, $submission);
        $submission->title = Variables::getParsedValue($form->settings->submissionTitleFormat, $submission, $form);
        if ($customTitle = Variables::getParsedValue($form->settings->submissionTitleFormat, $this, $form)) {
            $submission->title = Variables::getParsedValue($settings->submissionTitleFormat, $submission, $form);
            return Formie::$plugin->getTemplates()->renderObjectTemplate($value, $submission, $variables);
        $fileName = Formie::$plugin->getTemplates()->renderObjectTemplate($filenameFormat, $variables);
                // Ensure that we protect against developers using Twig to populate values from query strings
                    $value = Formie::$plugin->getTemplates()->renderString($value);