CVE-2022-39232: Other (NVD-CWE-Other)
value: Discourse is an open source discussion platform. Starting with version 2.9.0.beta5 and prior to version 2.9.0.beta10, an incomplete quote can generate a JavaScript error which will crash the current page in the browser in some cases. Version 2.9.0.beta10 added a fix and tests to ensure incomplete quotes won't break the app. As a workaround, the quote can be fixed via the rails console.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 4.3
Published: 2022-09-29T21:15Z
Repository: https://github.com/discourse/discourse
Language: JavaScript
Files changed: app/assets/javascripts/discourse/app/widgets/post-cooked.js, app/assets/javascripts/discourse/tests/integration/components/widgets/post-cooked-test.js, app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
Commit date: 2022-09-21 14:02:50 +0200
Commit message: SECURITY: Handle incomplete quote bbcode (#18311)


Diff: diff --git a/app/assets/javascripts/discourse/app/widgets/post-cooked.js b/app/assets/javascripts/discourse/app/widgets/post-cooked.js
index 8c52565dbb3b..754312cc291f 100644
--- a/app/assets/javascripts/discourse/app/widgets/post-cooked.js
+++ b/app/assets/javascripts/discourse/app/widgets/post-cooked.js
@@ -281,12 +281,13 @@ export default class PostCooked {
 
         this._updateQuoteElements($aside, "chevron-down");
         const $title = $(".title", $aside);
 
         // If post/topic is not found then display username, skip controls
-        if (e.classList.contains("quote-post-not-found")) {
-          return (e.querySelector(".title").innerHTML = e.dataset.username);
+        if (e.classList.contains("quote-post-not-found") && $title.length) {
+          e.querySelector(".title").innerHTML = e.dataset.username;
+          return;
         }
 
         // Unless it's a full quote, allow click to expand
         if (!($aside.data("full") || $title.data("has-quote-controls"))) {
           $title.on("click", (e2) => {
diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-cooked-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-cooked-test.js
new file mode 100644
index 000000000000..4967586e68c7
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-cooked-test.js
@@ -0,0 +1,21 @@
+import { module, test } from "qunit";
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import { render } from "@ember/test-helpers";
+import { query } from "discourse/tests/helpers/qunit-helpers";
+import { hbs } from "ember-cli-htmlbars";
+
+module("Integration | Component | Widget | post-cooked", function (hooks) {
+  setupRenderingTest(hooks);
+
+  test("quotes with no username and no valid topic", async function (assert) {
+    this.set("args", {
+      cooked: `<aside class=\"quote no-group quote-post-not-found\" data-post=\"1\" data-topic=\"123456\">\n<blockquote>\n<p>abcd</p>\n</blockquote>\n</aside>\n<p>Testing the issue</p>`,
+    });
+
+    await render(
+      hbs`<MountWidget @widget="post-cooked" @args={{this.args}} />`
+    );
+
+    assert.strictEqual(query("blockquote").innerText, "abcd");
+  });
+});
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
index 32c94e468bb8..3164a6a2abf2 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
@@ -440,10 +440,23 @@ eviltrout</p>
 </aside>`,
       "quote has group class"
     );
   });
 
+  test("Incomplete quotes", function (assert) {
+    assert.cookedOptions(
+      '[quote=", post: 1"]\na quote\n[/quote]',
+      { topicId: 2 },
+      `<aside class=\"quote no-group\" data-post=\"1\">
+<blockquote>
+<p>a quote</p>
+</blockquote>
+</aside>`,
+      "works with missing username"
+    );
+  });
+
   test("Mentions", function (assert) {
     assert.cooked(
       "Hello @sam",
       '<p>Hello <span class="mention">@sam</span></p>',
       "translates mentions to links"

Vulnerable code:         if (e.classList.contains("quote-post-not-found")) {
          return (e.querySelector(".title").innerHTML = e.dataset.username);
Fixed code:         if (e.classList.contains("quote-post-not-found") && $title.length) {
          e.querySelector(".title").innerHTML = e.dataset.username;
          return;
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { render } from "@ember/test-helpers";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { hbs } from "ember-cli-htmlbars";

module("Integration | Component | Widget | post-cooked", function (hooks) {
  setupRenderingTest(hooks);

  test("quotes with no username and no valid topic", async function (assert) {
    this.set("args", {
      cooked: `<aside class=\"quote no-group quote-post-not-found\" data-post=\"1\" data-topic=\"123456\">\n<blockquote>\n<p>abcd</p>\n</blockquote>\n</aside>\n<p>Testing the issue</p>`,
    });

    await render(
      hbs`<MountWidget @widget="post-cooked" @args={{this.args}} />`
    );

    assert.strictEqual(query("blockquote").innerText, "abcd");
  });
});
  test("Incomplete quotes", function (assert) {
    assert.cookedOptions(
      '[quote=", post: 1"]\na quote\n[/quote]',
      { topicId: 2 },
      `<aside class=\"quote no-group\" data-post=\"1\">
<blockquote>
<p>a quote</p>
</blockquote>
</aside>`,
      "works with missing username"
    );
  });