CVE-2020-11083: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: In October from version 1.0.319 and before version 1.0.466, a user with access to a markdown FormWidget that stores data persistently could create a stored XSS attack against themselves and any other users with access to the generated HTML from the field. This has been fixed in 1.0.466. For users of the RainLab.Blog plugin, this has also been fixed in 1.4.1.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 4.8
Published: 2020-07-14T21:15Z
Repository: https://github.com/octobercms/october
Language: PHP
Files changed: modules/backend/ServiceProvider.php, modules/backend/formwidgets/MarkdownEditor.php, modules/backend/lang/en/lang.php
Commit date: 2020-05-25 18:02:20 -0600
Commit message: Add new backend.allow_unsafe_markdown permission

Diff: diff --git a/modules/backend/ServiceProvider.php b/modules/backend/ServiceProvider.php
index 1a2dc4022..b0a5026f8 100644
--- a/modules/backend/ServiceProvider.php
+++ b/modules/backend/ServiceProvider.php
@@ -2,10 +2,11 @@
 
 use App;
 use Backend;
 use BackendMenu;
 use BackendAuth;
+use Backend\Models\UserRole;
 use Backend\Classes\WidgetManager;
 use System\Classes\MailManager;
 use System\Classes\CombineAssets;
 use System\Classes\SettingsManager;
 use October\Rain\Support\ModuleServiceProvider;
@@ -166,11 +167,16 @@ class ServiceProvider extends ModuleServiceProvider
                     'tab'   => 'system::lang.permissions.name'
                 ],
                 'media.manage_media' => [
                     'label' => 'backend::lang.permissions.manage_media',
                     'tab' => 'system::lang.permissions.name',
-                ]
+                ],
+                'backend.allow_unsafe_markdown' => [
+                    'label' => 'backend::lang.permissions.allow_unsafe_markdown',
+                    'tab' => 'system::lang.permissions.name',
+                    'roles' => UserRole::CODE_DEVELOPER,
+                ],
             ]);
         });
     }
 
     /*
diff --git a/modules/backend/formwidgets/MarkdownEditor.php b/modules/backend/formwidgets/MarkdownEditor.php
index c8222e14a..d6ec6accb 100644
--- a/modules/backend/formwidgets/MarkdownEditor.php
+++ b/modules/backend/formwidgets/MarkdownEditor.php
@@ -1,9 +1,10 @@
 <?php namespace Backend\FormWidgets;
 
-use BackendAuth;
+use Html;
 use Markdown;
+use BackendAuth;
 use Backend\Classes\FormWidgetBase;
 
 /**
  * Code Editor
  * Renders a code editor field.
@@ -40,16 +41,16 @@ class MarkdownEditor extends FormWidgetBase
     //
     // Object properties
     //
 
     /**
-     * @inheritDoc
+     * {@inheritDoc}
      */
     protected $defaultAlias = 'markdown';
 
     /**
-     * @inheritDoc
+     * {@inheritDoc}
      */
     public function init()
     {
         $this->fillFromConfig([
             'mode',
@@ -58,11 +59,11 @@ class MarkdownEditor extends FormWidgetBase
             'disabled',
         ]);
     }
 
     /**
-     * @inheritDoc
+     * {@inheritDoc}
      */
     public function render()
     {
         $this->prepareVars();
         return $this->makePartial('markdowneditor');
@@ -82,26 +83,58 @@ class MarkdownEditor extends FormWidgetBase
         $this->vars['disabled'] = $this->disabled;
         $this->vars['useMediaManager'] = BackendAuth::getUser()->hasAccess('media.manage_media');
     }
 
     /**
-     * @inheritDoc
+     * {@inheritDoc}
      */
     protected function loadAssets()
     {
         $this->addCss('css/markdowneditor.css', 'core');
         $this->addJs('js/markdowneditor.js', 'core');
         $this->addJs('/modules/backend/formwidgets/codeeditor/assets/js/build-min.js', 'core');
     }
 
+    /**
+     * Check to see if the generated HTML should be cleaned to remove any potential XSS
+     *
+     * @return boolean
+     */
+    protected function shouldCleanHtml()
+    {
+        $user = BackendAuth::getUser();
+        return !$user || !$user->hasAccess('backend.allow_unsafe_markdown');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getSaveValue($value)
+    {
+        if ($this->shouldCleanHtml()) {
+            $value = Html::clean($value);
+        }
+
+        return $value;
+    }
+
+    /**
+     * AJAX handler to render the markdown as HTML
+     *
+     * @return array ['preview' => $generatedHTML]
+     */
     public function onRefresh()
     {
         $value = post($this->getFieldName());
         $previewHtml = $this->safe
             ? Markdown::parseSafe($value)
             : Markdown::parse($value);
 
+        if ($this->shouldCleanHtml()) {
+            $previewHtml = Html::clean($previewHtml);
+        }
+
         return [
             'preview' => $previewHtml
         ];
     }
 }
diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php
index f4e933c03..1eca8313d 100644
--- a/modules/backend/lang/en/lang.php
+++ b/modules/backend/lang/en/lang.php
@@ -565,10 +565,11 @@ return [
             'windows_1252' => 'Windows-1252 (CP1252)',
         ],
     ],
     'permissions' => [
         'manage_media' => 'Upload and manage media contents - images, videos, sounds, documents',
+        'allow_unsafe_markdown' => 'Use unsafe Markdown (can use HTML & JS)',
     ],
     'mediafinder' => [
         'label' => 'Media Finder',
         'default_prompt' => 'Click the %s button to find a media item',
         'no_image' => 'The image could not be found',

Vulnerable code:                 ]
use BackendAuth;
     * @inheritDoc
     * @inheritDoc
     * @inheritDoc
     * @inheritDoc
Fixed code: use Backend\Models\UserRole;
                ],
                'backend.allow_unsafe_markdown' => [
                    'label' => 'backend::lang.permissions.allow_unsafe_markdown',
                    'tab' => 'system::lang.permissions.name',
                    'roles' => UserRole::CODE_DEVELOPER,
                ],
use Html;
use BackendAuth;
     * {@inheritDoc}
     * {@inheritDoc}
     * {@inheritDoc}
     * {@inheritDoc}
    /**
     * Check to see if the generated HTML should be cleaned to remove any potential XSS
     *
     * @return boolean
     */
    protected function shouldCleanHtml()
    {
        $user = BackendAuth::getUser();
        return !$user || !$user->hasAccess('backend.allow_unsafe_markdown');
    }

    /**
     * {@inheritDoc}
     */
    public function getSaveValue($value)
    {
        if ($this->shouldCleanHtml()) {
            $value = Html::clean($value);
        }

        return $value;
    }

    /**
     * AJAX handler to render the markdown as HTML
     *
     * @return array ['preview' => $generatedHTML]
     */
        if ($this->shouldCleanHtml()) {
            $previewHtml = Html::clean($previewHtml);
        }

        'allow_unsafe_markdown' => 'Use unsafe Markdown (can use HTML & JS)',