Sep 30, 2026·6 min read·19 visits
Unauthenticated algorithmic complexity vulnerability in league/commonmark allows CPU resource exhaustion and Denial of Service via crafted Markdown paragraphs.
An algorithmic complexity vulnerability in the `league/commonmark` library's GitHub Flavored Markdown (GFM) Table extension allows remote, unauthenticated attackers to cause a Denial of Service (DoS) via high CPU consumption. By submitting a specially crafted Markdown payload containing an extremely long paragraph without letter characters or pipe symbols, the parser executes a quadratic-time $O(N^2)$ scanning operation that exhausts system resources.
The GitHub Flavored Markdown (GFM) Table extension within the PHP league/commonmark library contains an algorithmic complexity flaw. This flaw enables remote attackers to exhaust CPU resources on the hosting server, resulting in a Denial of Service. The vulnerability lies specifically within the table start parser, which attempts to determine if an active paragraph represents the initiation of a GFM table container.\n\nUnder standard conditions, markdown paragraphs are processed line-by-line as they grow. When the GFM Table extension is enabled, the library invokes the table parser on every line to evaluate potential table syntax. Because the parser performs an unconstrained scan of the entire accumulated paragraph on every iteration, the time complexity scales quadratically ($O(N^2)$) relative to the number of lines $N$ within the paragraph block.\n\nThis behavior classifies the vulnerability under CWE-400 (Uncontrolled Resource Consumption) and CWE-1333 (Inefficient Regular Expression Complexity / Algorithmic Complexity). The exposure surface is significant because many web applications leverage league/commonmark to render user-provided Markdown content in forums, comments, and documentation portals without prior input length validation.
The technical root cause centers on the TableStartParser::tryStart() method. The parser is designed to identify the start of a GFM table, which requires detecting a header row followed by a delimiter row. However, a paragraph block remains active and continues to grow until a blank line or a different block-level container interrupts it. Each time a new line is added to this active paragraph, the library queries the table block-start parser.\n\nInside TableStartParser::tryStart(), the code calls $parserState->getParagraphContent() to retrieve the entire accumulated paragraph content as a single string. It then performs a quick-check lookup using the PHP native function \\strpos($paragraph, '|'). If the pipe character is not found, the parser immediately returns, indicating that no table starts here.\n\nWhile this check is designed to quickly discard non-table lines, executing it on the entire growing paragraph buffer creates the performance bottleneck. On line 1, \\strpos scans line 1. On line 2, it scans lines 1 and 2. On line $N$, it scans the entire block of $N$ lines. If the paragraph is extremely long and contains no pipe characters, the total scanning time becomes quadratic with respect to the number of lines.\n\nAn important constraint exists within this execution path. A pre-evaluation filter called SkipLinesStartingWithLettersParser evaluates incoming lines first. If a line begins with a standard alphabetical character, this parser short-circuits the table evaluation. Consequently, the quadratic-time vulnerability can only be triggered if the payload contains lines starting with non-alphabetical characters, such as numbers, spaces, or multibyte Unicode characters.
The vulnerable implementation of TableStartParser::tryStart() in versions prior to 2.10.2 highlights the scanning inefficiency. The method attempts to locate a pipe character within the entire historical paragraph string before verifying the current line's validity. This is demonstrated in the pre-patch source code:\n\nphp\npublic function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart\n{\n $paragraph = $parserState->getParagraphContent();\n if ($paragraph === null || \\strpos($paragraph, '|') === false) {\n return BlockStart::none();\n }\n\n\nTo fix this behavior, the maintainers isolated the table header checking logic. In Markdown GFM table specifications, only the line immediately preceding the table delimiter row can serve as the table header. Therefore, there is no technical requirement to scan the preceding lines of an active paragraph. The patch targets the last line of the paragraph exclusively.\n\nThe corrected implementation uses \\strrpos() to locate the last newline character in the accumulated paragraph. It then extracts only the final line of text using \\substr() and performs the \\strpos() check on that segment alone. This modification guarantees that the scan length is limited to the length of the current line, $O(L)$, reducing the overall parsing time complexity from quadratic $O(N^2)$ to linear $O(N).\n\ndiff\n- if ($paragraph === null || \\strpos($paragraph, '|') === false) {\n+ if ($paragraph === null) {\n+ return BlockStart::none();\n+ }\n+\n+ // Only the paragraph's last line can be the header row, so limit the quick check to it.\n+ // Scanning the whole (growing) paragraph on every line would be quadratic.\n+ $lastLineBreak = \\strrpos($paragraph, \"\\n\");\n+ $lastLine = $lastLineBreak === false ? $paragraph : \\substr($paragraph, $lastLineBreak + 1);\n+ if (\\strpos($lastLine, '|') === false) {\n return BlockStart::none();\n }\n
An attacker can exploit this vulnerability by submitting a highly dense Markdown payload to any application endpoint that parses GFM Markdown. The target configuration must have the GFM TableExtension enabled. The attack requires no authentication or special privileges.\n\nThe crafted payload must fulfill four specific criteria to bypass optimization structures and trigger the vulnerable loop. First, the input must contain thousands of lines. Second, none of the lines may begin with standard alphabetical characters. Third, none of the lines may contain a pipe symbol. Fourth, the paragraph must remain unbroken by empty lines.\n\nStandard proof-of-concept payloads utilize repeated lines of numerical sequences or multibyte Unicode sequences. When the parser processes these inputs, the CPU utilization of the executing PHP worker process spikes to 100%. If an attacker submits multiple requests concurrently, the server's thread pool becomes completely exhausted, resulting in a global denial of service for all users.\n\nHere is an illustration of the execution flow during an attack:\n\nmermaid\ngraph LR\n A[\"Client sends payload\"] --> B[\"Markdown parser initialized\"]\n B --> C[\"TableStartParser processes each line\"]\n C --> D{\"Line starts with letter?\"}\n D -- \"Yes\" --> E[\"Short-circuit evaluation\"]\n D -- \"No\" --> F[\"Scan entire accumulated paragraph for '|'\"]\n F --> G{\"Is paragraph long?\"}\n G -- \"Yes (No '|' found)\" --> H[\"Quadratic-time O(N^2) CPU loop\"]\n G -- \"No\" --> I[\"Process next line\"]\n H --> J[\"CPU resource exhaustion & Thread lock\"]\n
The primary impact of this vulnerability is a complete Denial of Service (DoS) of the hosting application. Because PHP processes are typically single-threaded and capped by pool limits, a small number of concurrent exploit payloads can easily exhaust all available PHP worker threads. This blocks legitimate incoming HTTP traffic, leading to timeout errors and system downtime.\n\nThe vulnerability is remediated by upgrading league/commonmark to version 2.10.2 or higher. This release contains the patch that limits string scanning to the last line of the active paragraph block. If upgrading is not immediately feasible, developers can mitigate the risk by disabling the TableExtension from the CommonMark environment configuration.\n\nIn addition to patching, implementing Web Application Firewall (WAF) rules or rate-limiting mechanisms can provide defense-in-depth. Security teams should monitor for POST requests containing exceptionally large payloads with highly repetitive numerical or multibyte lines. Input validation that restricts maximum submission sizes before parsing further reduces the attack surface.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
league/commonmark thephpleague | >= 2.0.0, < 2.10.2 | 2.10.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 / CWE-1333 |
| Attack Vector | Network (Unauthenticated) |
| CVSS Score | 7.5 |
| Impact | Denial of Service (DoS) via CPU Exhaustion |
| Exploit Status | Proof-of-Concept (PoC) Verified |
| KEV Status | Not Listed |
The software does not properly control the allocation and maintenance of a limited resource, enabling an actor to influence the amount of resources consumed and eventually leading to exhaustion.
CVE-2026-128087 (GHSA-9264-jvgf-6w2f) is an incorrect behavior order (CWE-696) and improper authorization (CWE-863) vulnerability in the russh Rust SSH library, affecting versions 0.55.0 through 0.65.0. During the initial SSH key exchange (KEX) handshake, the client state machine invokes the client::Handler::kex_done() callback—providing the negotiated raw shared secret bytes—prior to validating the remote server's host key or certificate via check_server_key(). Consequently, an untrusted or rogue SSH server can complete cryptographic key agreement and force a connecting client to execute side-effecting application logic inside kex_done() before the server's identity is checked and rejected.
CVE-2026-128082 (GHSA-6vhm-j9h7-f9gh) is an uncontrolled recursion vulnerability (CWE-674) in AutoMapper, a convention-based object-object mapper for .NET. When mapping nested or self-referential types into destination models that rely on parameterized constructors, C# positional records, or recursive ConstructUsing expressions, AutoMapper evaluates child constructor arguments prior to incrementing the parent's recursion depth counter. This ordering flaw bypasses both explicit MaxDepth limits and the default depth limit of 64. An unauthenticated remote attacker can supply deeply nested input payloads to affected endpoints, exhausting the thread call stack and triggering an uncatchable System.StackOverflowException that terminates the hosting .NET process.
An information exposure vulnerability in the @opentelemetry/instrumentation-http package for OpenTelemetry JavaScript versions 0.204.0 prior to 0.222.0 causes incoming HTTP server spans to record raw, unredacted query string parameters under the url.query attribute. While outgoing client spans applied built-in and user-configured query parameter redaction, incoming server spans bypassed this sanitization completely, resulting in the transmission of sensitive credentials such as pre-signed cloud URLs and API tokens to external observability backends.
CVE-2026-128089 is an uncontrolled resource consumption vulnerability in the dnspython toolkit for Python, affecting all versions prior to 2.9.2. When parsing Well-Known Service (WKS) records from text via dns.rdtypes.IN.WKS.WKS.from_text(), the parser fails to validate that numeric service port identifiers fall within the RFC 1035 valid range of 0 through 65535. An attacker capable of supplying untrusted zone files or RDATA text strings containing an arbitrarily large service port integer can force the parser to allocate an unbounded bitmap array using an inefficient linear append loop, resulting in severe CPU exhaustion, application hangs, or Out-Of-Memory (OOM) process termination.
CVE-2026-126347 (GHSA-5cjw-f85j-cv6r) is an open redirect vulnerability (CWE-601) in Tornado, an asynchronous Python networking library and web framework. The flaw resides in tornado.web.RedirectHandler when dynamic URL redirection templates incorporate unvalidated user input. Due to a parser differential between RFC 3986 (used by Python's urllib.parse.urlsplit) and the WHATWG URL Standard (used by modern web browsers), inputs containing three or more leading slashes bypass Tornado 6.6.0's validation logic, allowing unauthenticated attackers to redirect users to arbitrary external domains.
A Denial of Service (DoS) vulnerability exists in @sveltejs/adapter-node versions 5.5.7 and 6.0.0-next.0 through 6.0.0. When a SvelteKit server route returns a direct response from a fetch() call with a Content-Type of text/event-stream, the Node.js adapter attempts to mutate the response headers to disable proxy buffering. Because headers returned directly by the WHATWG Fetch API carry an immutable guard, this mutation throws an unhandled TypeError, terminating the underlying Node.js process and denying service to all active and subsequent clients.