CVE-2021-43827 (published 2021-12-14T23:15Z)

discourse-footnote is a library providing footnotes for posts in Discourse. ### Impact When posting an inline footnote wrapped in `<a>` tags (e.g. `<a>^[footnote]</a>`, the resulting rendered HTML would include a nested `<a>`, which is stripped by Nokogiri because it is not valid. This then caused a javascript error on topic pages because we were looking for an `<a>` element inside the footnote reference span and getting its ID, and because it did not exist we got a null reference error in javascript. Users are advised to update to version 0.2. As a workaround editing offending posts from the rails console or the database console for self-hosters, or disabling the plugin in the admin panel can mitigate this issue.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:N/A:P)
CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L)

Weakness information:
cwe_id: CWE-755
cwe_name: Improper Handling of Exceptional Conditions
description: The product does not handle or incorrectly handles an exceptional condition.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/discourse/discourse-footnote
commit_details: repo_url: https://github.com/discourse/discourse-footnote
author: Martin Brennan
author_date: 2021-12-14 15:35:11+10:00
msg: SECURITY: Fix malicious footnote causing clientside errors (#27)

Some malformed/malicious footnote markup could cause
clientside errors.
repository: repo_url: https://github.com/discourse/discourse-footnote
repo_name: discourse/discourse-footnote
description: footnotes for posts in Discourse
repo_language: Ruby
owner: discourse
file_changes: filename: inline-footnotes.js
change_type: ModificationType.MODIFY
diff: @@ -8,14 +8,20 @@ function applyInlineFootnotes(elem) {
   const footnoteRefs = elem.querySelectorAll("sup.footnote-ref");
 
   footnoteRefs.forEach((footnoteRef) => {
+    const refLink = footnoteRef.querySelector("a");
+    if (!refLink) {
+      return;
+    }
+
     const expandableFootnote = document.createElement("a");
     expandableFootnote.classList.add("expand-footnote");
     expandableFootnote.innerHTML = iconHTML("ellipsis-h");
     expandableFootnote.href = "";
     expandableFootnote.role = "button";
-    expandableFootnote.dataset.footnoteId = footnoteRef
-      .querySelector("a")
-      .id.replace("footnote-ref-", "");
+    expandableFootnote.dataset.footnoteId = refLink.id.replace(
+      "footnote-ref-",
+      ""
+    );
 
     footnoteRef.after(expandableFootnote);
   });

programming_language: TypeScript
filename: markdown-it-footnote.js
change_type: ModificationType.MODIFY
diff: @@ -212,6 +212,15 @@ module.exports = function footnote_plugin(md) {
         tokens = []
       );
 
+      // Having a rendered footnote inside a link creates a nested link, which
+      // is not valid HTML, so close the parent tag first before proceeding.
+      const previousToken = state.tokens[state.tokens.length - 1];
+      if (previousToken?.content.includes("<a")) {
+        const linkCloseToken = state.push("html_inline", "", 0);
+        linkCloseToken.content = "</a>";
+        linkCloseToken.block = false;
+      }
+
       token      = state.push('footnote_ref', '', 0);
       token.meta = { id: footnoteId };
 

programming_language: JavaScript
filename: plugin.rb
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,7 @@
 # transpile_js: true
 # name: discourse-footnote
 # about: Adds markdown.it footnote support to Discourse
-# version: 0.1
+# version: 0.2
 # authors: Sam Saffron, Vitaly Puzrin
 # url: https://github.com/discourse/discourse-footnote
 

programming_language: Ruby
filename: pretty_text_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -87,4 +87,31 @@ describe PrettyText do
 
   end
 
+  it "supports inline footnotes wrapped in <a> elements by ending the elements early" do
+    raw = <<~MD
+      I have a point, see footnote. <a>^[the point]</a>
+
+      <a>^[footnote]</a>
+    MD
+
+    post = create_post(raw: raw)
+    post.reload
+
+    html = <<~HTML
+      <p>I have a point, see footnote. <a></a><sup class="footnote-ref"><a href="#footnote-#{post.id}-1" id="footnote-ref-#{post.id}-1">[1]</a></sup></p>
+      <p><a></a><sup class="footnote-ref"><a href="#footnote-#{post.id}-2" id="footnote-ref-#{post.id}-2">[2]</a></sup></p>
+      <hr class="footnotes-sep">
+
+      <ol class="footnotes-list">
+      <li id="footnote-#{post.id}-1" class="footnote-item">
+      <p>the point <a href="#footnote-ref-#{post.id}-1" class="footnote-backref">↩︎</a></p>
+      </li>
+      <li id="footnote-#{post.id}-2" class="footnote-item">
+      <p>footnote <a href="#footnote-ref-#{post.id}-2" class="footnote-backref">↩︎</a></p>
+      </li>
+      </ol>
+    HTML
+
+    expect(post.cooked.strip).to eq(html.strip)
+  end
 end

programming_language: HTML