{"id":"GHSA-j95f-988m-3j2f","summary":"Tiptap: Quadratic ReDoS in block and inline Markdown attribute parsing","details":"## 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.","modified":"2026-09-08T21:30:04.763852956Z","published":"2026-09-08T21:24:09Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-09-08T21:24:09Z","nvd_published_at":null,"cwe_ids":["CWE-1333","CWE-400"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/ueberdosis/tiptap/security/advisories/GHSA-j95f-988m-3j2f"},{"type":"WEB","url":"https://github.com/ueberdosis/tiptap/commit/d0d499be3cce633cf54ca9aa9f3d8a5a1f98bd74"},{"type":"PACKAGE","url":"https://github.com/ueberdosis/tiptap"},{"type":"WEB","url":"https://github.com/ueberdosis/tiptap/releases/tag/v3.30.5"}],"affected":[{"package":{"name":"@tiptap/core","ecosystem":"npm","purl":"pkg:npm/%40tiptap/core"},"ranges":[{"type":"SEMVER","events":[{"introduced":"3.7.0"},{"fixed":"3.30.5"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-j95f-988m-3j2f/GHSA-j95f-988m-3j2f.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}