{"id":"GHSA-j95f-988m-3j2f","title":"Tiptap: Quadratic ReDoS in block and inline Markdown attribute parsing","summary":"Tiptap: Quadratic ReDoS in block and inline Markdown attribute parsing","severity":"high","cwe":["CWE-400","CWE-1333"],"vendor":"tiptap","product":"@tiptap/core","ecosystem":"npm","affected":["@tiptap/core >= 3.7.0, < 3.30.5"],"patched":["@tiptap/core 3.30.5"],"published":"2026-09-08","updated":"2026-09-08","sourceUpdated":"2026-09-08T21:24:11Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-j95f-988m-3j2f","references":[{"url":"https://github.com/ueberdosis/tiptap/security/advisories/GHSA-j95f-988m-3j2f"},{"url":"https://github.com/ueberdosis/tiptap/commit/d0d499be3cce633cf54ca9aa9f3d8a5a1f98bd74"},{"url":"https://github.com/ueberdosis/tiptap/releases/tag/v3.30.5"},{"url":"https://github.com/advisories/GHSA-j95f-988m-3j2f"}],"tags":["ghsa","npm"],"ingestedAt":"2026-09-08T22:12:30.968Z","slug":"GHSA-j95f-988m-3j2f","body":"## Overview\n\n## Summary\n\n`@tiptap/core` contains two quadratic regular-expression denial-of-service paths in its default Markdown attribute parsers. Pandoc-style block attributes use two unanchored greedy expressions that rescan repeated `__QUOTED_0` prefixes. Inline shortcode attributes use another unanchored greedy key expression that rescans a long word-character run when no equals sign follows.\n\nThe public `createAtomBlockMarkdownSpec` and `createBlockMarkdownSpec` helpers call the vulnerable Pandoc-style parser; `createInlineMarkdownSpec` calls the separately vulnerable shortcode parser. Using unmodified npm 3.29.2, a complete 20,508-byte atom-block token took approximately 1.40 seconds while an equal-length control took 0.29 ms. A complete 32,776-byte inline token took approximately 2.21 seconds while its equal-length control took 0.19 ms. Current repository `main` commit `5158212970344952dd9918b6a44bfb400d7fb6c1` retains both expressions.\n\n## Block attribute root cause\n\n`packages/core/src/utilities/markdown/attributeUtils.ts` uses both `matchAll` and `replace` with `/([a-zA-Z][\\w-]*)\\s*=\\s*(__QUOTED_\\d+__)/g`. The candidate is `'__QUOTED_0'.repeat(n) + '__QUOTED_0__'`. There are no quotes, so the preceding replacement leaves it unchanged. At each `Q`, the greedy key-name expression consumes the remaining word-character run, the required equals sign fails, and the unanchored engine restarts at the next `Q`. This yields `O(n^2)` work, and the cleanup pass repeats it.\n\nA complete public-API proof is:\n\n```js\nimport { createAtomBlockMarkdownSpec } from '@tiptap/core'\nconst tokenizer = createAtomBlockMarkdownSpec({ nodeName: 'probe' }).markdownTokenizer\nconst attack = '__QUOTED_0'.repeat(2048) + '__QUOTED_0__'\nconst source = `:::probe {${attack}} :::\\n`\nconst started = performance.now()\ntokenizer.tokenize(source, [], {})\nconsole.log(performance.now() - started)\n```\n\nMeasured complete-tokenizer timings were 6.23, 23.12, 88.93, 369.10, and 1,400.17 ms at 1,308, 2,588, 5,148, 10,268, and 20,508 bytes. Equal-length controls took 0.07 to 0.29 ms. The directly exported parser took 5,645.71 ms at 40,972 bytes while its control took 0.64 ms.\n\n## Inline attribute root cause\n\n`packages/core/src/utilities/markdown/createInlineMarkdownSpec.ts` uses `/(\\w+)=(?:\"([^\"]*)\"|'([^']*)')/g`. For a long word-character run without an equals sign, `\\w+` consumes the remaining suffix, `=` fails, and the unanchored engine restarts at the next character. The default inline tokenizer extracts this attacker string directly from a syntactically complete `[shortcode attributes]` token.\n\n```js\nimport { createInlineMarkdownSpec } from '@tiptap/core'\nconst tokenizer = createInlineMarkdownSpec({ nodeName: 'probe', selfClosing: true }).markdownTokenizer\nconst source = `[probe ${'0'.repeat(32768)}]`\nconst started = performance.now()\ntokenizer.tokenize(source, [], {})\nconsole.log(performance.now() - started)\n```\n\nAt 1,032, 2,056, 4,104, 8,200, 16,392, and 32,776 bytes, candidates took 3.24, 12.88, 54.82, 136.91, 557.83, and 2,209.47 ms. Equal-length hyphen controls took 0.02 to 0.19 ms.\n\n## Impact\n\nApplications parsing attacker-controlled Markdown with these helpers can have a browser main thread, server event loop, or worker blocked by a small input. Persisted documents can repeatedly freeze clients; repeated requests can exhaust server-side parsing capacity. Editors that only consume validated ProseMirror JSON and never invoke the Markdown parsing path are not directly affected through document content.\n\n## History and remediation\n\nCommit `35645d94ae9cd73448a564104c2e08f64e9564bc` introduced both parsers on 14 October 2025, first released in 3.7.0. Versions 3.7.0 through current 3.29.2 and current `main` remain affected. Official issue, PR, and repository-advisory searches found no duplicate.\n\nRequire a start-of-string or whitespace boundary before both key-value parsers, and preferably replace the multi-pass placeholder and shortcode regex designs with deterministic single-pass tokenizers. Keep quoted values out-of-band so attacker input cannot collide with predictable `__QUOTED_n__` placeholders. Add complete block and inline Markdown-tokenizer scaling regressions with equal-length controls.\n\nPlease credit GitHub user `joostgrunwald` as finder/reporter.\n\n## Affected packages\n\n- `@tiptap/core >= 3.7.0, < 3.30.5`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `@tiptap/core 3.30.5`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":41.3,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}