CVE-2021-37633: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Discourse is an open source discussion platform. In versions prior to 2.7.8 rendering of d-popover tooltips can be susceptible to XSS attacks. This vulnerability only affects sites which have modified or disabled Discourse's default Content Security Policy. This issue is patched in the latest `stable` 2.7.8 version of Discourse. As a workaround users may ensure that the Content Security Policy is enabled, and has not been modified in a way which would make it more vulnerable to XSS attacks.
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: 6.1
Published: 2021-08-09T20:15Z
Repository: https://github.com/discourse/discourse
Language: JavaScript
Files changed: app/assets/javascripts/discourse/app/lib/d-popover.js, app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js, app/assets/javascripts/pretty-text/addon/sanitizer.js
Commit date: 2021-08-05 16:39:17 +0300
Commit message: SECURITY: Sanitize d-popover attributes (#13958)


Diff: diff --git a/app/assets/javascripts/discourse/app/lib/d-popover.js b/app/assets/javascripts/discourse/app/lib/d-popover.js
index 19e47c1ca0..5438fc885e 100644
--- a/app/assets/javascripts/discourse/app/lib/d-popover.js
+++ b/app/assets/javascripts/discourse/app/lib/d-popover.js
@@ -14,12 +14,11 @@ const D_POPOVER_TEMPLATE = `
 
 const D_ARROW_HEIGHT = 10;
 
 const D_HORIZONTAL_MARGIN = 5;
 
-export const POPOVER_SELECTORS =
-  "[data-html-popover], [data-html-tooltip], [data-popover], [data-tooltip]";
+export const POPOVER_SELECTORS = "[data-popover], [data-tooltip]";
 
 export function hidePopover() {
   getPopover().fadeOut().remove();
 
   return getPopover();
@@ -53,15 +52,10 @@ export function showPopover(event, options = {}) {
     hide: hidePopover,
   };
 }
 
 function setPopoverHtmlContent($enteredElement, content) {
-  content =
-    content ||
-    $enteredElement.attr("data-html-popover") ||
-    $enteredElement.attr("data-html-tooltip");
-
   replaceHtmlContent($enteredElement, content);
 }
 
 function setPopoverTextContent($enteredElement, content) {
   content =
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js b/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js
index 27aac256de..f148bdb36b 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/sanitizer-test.js
@@ -134,10 +134,15 @@ module("Unit | Utility | sanitizer", function () {
       `<sup>high</sup> <sub>low</sub> <big>HUGE</big>`,
       `<p><sup>high</sup> <sub>low</sub> <big>HUGE</big></p>`
     );
 
     cooked(`<div dir="rtl">RTL text</div>`, `<div dir="rtl">RTL text</div>`);
+
+    cooked(
+      `<div data-value="<something>" data-html-value="<something>"></div>`,
+      `<div data-value="&lt;something&gt;"></div>`
+    );
   });
 
   test("ids on headings", function (assert) {
     const pt = new PrettyText(buildOptions({ siteSettings: {} }));
     assert.equal(pt.sanitize("<h3>Test Heading</h3>"), "<h3>Test Heading</h3>");
diff --git a/app/assets/javascripts/pretty-text/addon/sanitizer.js b/app/assets/javascripts/pretty-text/addon/sanitizer.js
index 45342882d5..1b61c6c243 100644
--- a/app/assets/javascripts/pretty-text/addon/sanitizer.js
+++ b/app/assets/javascripts/pretty-text/addon/sanitizer.js
@@ -73,11 +73,13 @@ export function sanitize(text, allowLister) {
       if (forTag) {
         const forAttr = forTag[name];
         if (
           (forAttr &&
             (forAttr.indexOf("*") !== -1 || forAttr.indexOf(value) !== -1)) ||
-          (name.indexOf("data-") === 0 && forTag["data-*"]) ||
+          (name.indexOf("data-html-") === -1 &&
+            name.indexOf("data-") === 0 &&
+            forTag["data-*"]) ||
           (tag === "a" &&
             name === "href" &&
             hrefAllowed(value, extraHrefMatchers)) ||
           (tag === "img" &&
             name === "src" &&

Vulnerable code: export const POPOVER_SELECTORS =
  "[data-html-popover], [data-html-tooltip], [data-popover], [data-tooltip]";
  content =
    content ||
    $enteredElement.attr("data-html-popover") ||
    $enteredElement.attr("data-html-tooltip");

          (name.indexOf("data-") === 0 && forTag["data-*"]) ||
Fixed code: export const POPOVER_SELECTORS = "[data-popover], [data-tooltip]";

    cooked(
      `<div data-value="<something>" data-html-value="<something>"></div>`,
      `<div data-value="&lt;something&gt;"></div>`
    );
          (name.indexOf("data-html-") === -1 &&
            name.indexOf("data-") === 0 &&
            forTag["data-*"]) ||