Quadratic Time Denial of Service in league/commonmark Table Extension
An unauthenticated remote attacker can cause denial of service by submitting large Markdown paragraphs that trigger O(M^2) CPU consumption in the league/commonmark TableStartParser.
Triage facts
- Applicability
- commonmark (>= 2.0.0, <= 2.10.1)
- Severity
- Medium
- Action
- Elevated
- Freshness
- PublishedSep 30, 16:27 UTC
The league/commonmark library, specifically in versions 2.0.0 through 2.10.1, contains a quadratic-time complexity vulnerability in its GitHub Flavored Markdown (GFM) Table extension. The TableStartParser performs a full-buffer scan of the accumulated paragraph on every new line via strpos() to check for potential table headers. Because the paragraph buffer grows indefinitely as long as non-blank lines are provided, and the scan traverses this entire buffer repeatedly, the work required grows quadratically (O(M^2)) relative to the input size. An unauthenticated attacker can exploit this by submitting large paragraphs consisting of lines that do not start with a letter (bypassing the SkipLinesStartingWithLettersParser) and contain no pipe characters. This causes excessive CPU usage, which can exhaust available PHP worker processes and result in a denial of service.
Attack Chain
- Exposure: The attacker identifies an application using
league/commonmark(specifically withGithubFlavoredMarkdownConverteror theTableExtensionenabled) that processes untrusted Markdown content. - Control: The attacker prepares a large Markdown body consisting of a single paragraph with no blank lines, no pipe (
|) characters, and lines beginning with non-letter characters (e.g., digits). - Path: As the parser processes the input, the
ParagraphParserkeeps the paragraph block open, causing the library to append each line to the growingparagraphbuffer. - Bypass: Since the input lines do not begin with a letter, the
SkipLinesStartingWithLettersParserreturnsBlockStart::abort(), allowing the parser to continue searching for other block types. - Primitive: The
MarkdownParserdispatches the input to theTableStartParser::tryStart()on every line, which executesstrpos($paragraph, '|')against the entire, continuously growing buffer. - Guard Absence: No input-size caps or effective length guards prevent the cumulative buffer growth or the exhaustive scan, allowing the O(M^2) complexity to manifest.
- Result: The cumulative processing time leads to extreme CPU load, effectively exhausting server resources and denying service to legitimate users.
Impact
Successful exploitation results in a denial of service by consuming all available server resources or PHP worker threads. The impact is limited to availability, with no risk to data confidentiality or integrity. Applications rendering untrusted Markdown from external users are at the highest risk. Measured benchmarks demonstrate that doubling a multi-megabyte input quadruples the CPU time, with 200,000 lines taking approximately 27.81 seconds to parse.
Recommendation
- Upgrade
league/commonmarkto a version where this vulnerability is remediated (as of this writing, no fix is available; monitor the upstream repository for updates). - Implement application-level constraints on the total length of user-submitted Markdown content before passing it to the converter.
- If not required, disable the
TableExtensionin theGithubFlavoredMarkdownConverterconfiguration when processing untrusted input. - Monitor application server CPU usage patterns specifically for PHP-FPM worker saturation coinciding with requests to Markdown rendering endpoints.
Immediate actions
Audit applications using league/commonmark and implement input length limits as an interim mitigation.
Mitigations
Disable the Table extension for untrusted input processing until a patch is applied.
O(M^2) complexity path in TableStartParser