Back to Blog
high SEVERITY9 min read

How Denial of Service via Adjacent Inline Attribute Blocks Happens in PHP and how to fix it

A high-severity denial-of-service vulnerability (GHSA-g2gp-3wwq-f4ph) was discovered in `league/commonmark`, a popular PHP Markdown parsing library. The flaw allows an attacker to craft Markdown input containing adjacent inline attribute blocks that trigger catastrophic processing, potentially exhausting server resources. Upgrading from version 2.7.1 to 2.9.0 resolves the issue by hardening how the parser handles these malformed constructs.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

GHSA-g2gp-3wwq-f4ph is a high-severity Denial of Service vulnerability in the PHP library `league/commonmark` (CWE-400: Uncontrolled Resource Consumption). In versions prior to 2.9.0, the inline attribute block parser can be triggered into excessive computation when processing specially crafted Markdown containing adjacent `{...}` attribute blocks. The fix is to upgrade `league/commonmark` to version 2.9.0, which tightens the parsing logic so malformed adjacent attribute blocks no longer cause runaway processing. In `composer.json`, the version constraint was changed from `^2.1` to the pinned `2.9.0` to ensure the vulnerable range is excluded.

Vulnerability at a Glance

cweCWE-400 (Uncontrolled Resource Consumption)
fixUpgrade league/commonmark from 2.7.1 to 2.9.0, which fixes the unbounded parsing loop in the attribute block handler
riskAttackers can submit crafted Markdown to exhaust CPU/memory and take down the application
languagePHP
root causeThe inline attribute block parser in league/commonmark did not bound or short-circuit processing of adjacent `{...}` blocks, leading to excessive resource consumption
vulnerabilityDenial of Service via Adjacent Inline Attribute Blocks

How Denial of Service via Adjacent Inline Attribute Blocks Happens in PHP and how to fix it


Vulnerability at a Glance

Field Detail
Vulnerability Denial of Service via Adjacent Inline Attribute Blocks
CWE CWE-400 — Uncontrolled Resource Consumption
Language PHP
Risk Attacker-supplied Markdown exhausts server CPU/memory
Root Cause Unbounded processing of consecutive {...} inline attribute blocks
Fix Upgrade league/commonmark from 2.7.12.9.0

Quick Answer

GHSA-g2gp-3wwq-f4ph is a high-severity DoS in league/commonmark < 2.9.0 (CWE-400). The inline attribute block parser performs unbounded work when it encounters adjacent {...} blocks in Markdown, allowing any user who can submit Markdown input to hang or crash the server. Fix: pin league/commonmark to 2.9.0 in composer.json and regenerate composer.lock.


Introduction

The composer.lock file in this PHP project recorded league/commonmark at version 2.7.1 — and locked inside that version was a ticking clock. The league/commonmark library is one of the most widely used Markdown-to-HTML parsers in the PHP ecosystem, powering content rendering in CMS platforms, documentation sites, and SaaS applications. When it parses Markdown, it walks the document tree and applies inline parsers to handle special syntax like links, emphasis, and — crucially — inline attribute blocks written as {.class #id key=value}.

The vulnerability tracked as GHSA-g2gp-3wwq-f4ph lives in exactly that attribute-block parsing path. When the parser encounters multiple attribute blocks placed adjacent to one another — think {.foo}{.bar}{.baz} repeated many times — it enters a processing loop that does not properly bound its work. The result is that a tiny, carefully constructed Markdown payload can drive CPU utilization to 100% and hold it there, effectively denying service to every other user of the application.

This post walks through what the vulnerability is, how it can be exploited, and exactly what changed in the composer.json and composer.lock to close it.


The Vulnerability Explained

What Are Inline Attribute Blocks?

league/commonmark supports an extended Markdown syntax (from the CommonMark Attributes extension) that lets authors attach HTML attributes to inline elements using curly-brace blocks:

This is *emphasized*{.highlight} text.

The {.highlight} block tells the renderer to add class="highlight" to the <em> tag. This is a legitimate, useful feature. The parser scans the inline content character by character, and when it sees {, it tries to consume an attribute block.

Where the Problem Lives

The vulnerability arises when multiple attribute blocks appear consecutively with no intervening content:

text{.a}{.b}{.c}{.d}{.e}...{.z}

In league/commonmark 2.7.1, the inline attribute block parser does not correctly short-circuit or bound the number of consecutive attribute blocks it will attempt to process in a single pass. Each {...} block triggers a new parsing attempt, and the interaction between the parser's backtracking logic and the sequential block structure causes the time complexity to grow super-linearly — potentially exponentially — with the number of adjacent blocks.

This is a classic ReDoS-adjacent pattern: not a regex catastrophic backtrack per se, but a parsing loop whose work grows unboundedly with attacker-controlled repetition in the input.

A Concrete Attack Payload

An attacker who can submit any Markdown to the application — a comment field, a wiki page, a ticket description, an API endpoint that accepts Markdown — can send something like:

x{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}{.a}

A payload of just a few hundred bytes can cause the PHP process handling the request to spin at 100% CPU for seconds, minutes, or indefinitely — depending on the server's timeout configuration. With a handful of concurrent requests, an attacker can saturate all available PHP workers and render the application completely unavailable to legitimate users.

Real-World Impact for This Application

This project uses league/commonmark directly as a production dependency ("require" in composer.json, not "require-dev"). That means any code path that calls the CommonMark parser on user-supplied content is a potential DoS vector. Given that CommonMark is typically used to render user-generated content — exactly the scenario where untrusted input flows in — the exposure surface is significant.


The Fix

What Changed

Two files were modified: composer.json and composer.lock.

composer.json — Tightening the Version Constraint

Before:

"require": {
    "league/commonmark": "^2.1",
    "rlanvin/php-rrule": "^2.3"
}

After:

"require": {
    "league/commonmark": "2.9.0",
    "rlanvin/php-rrule": "^2.3"
}

The original constraint ^2.1 allowed any version from 2.1.0 up to (but not including) 3.0.0. That range includes every vulnerable 2.x release. By pinning to the exact version 2.9.0, the project guarantees that only the patched release is ever installed — no future composer update can silently pull in a vulnerable version.

Why pin exactly instead of using ^2.9?
Pinning to 2.9.0 is the most conservative choice: it ensures reproducibility and prevents any future 2.x release (which might introduce new issues) from being installed without an explicit, reviewed version bump. This is a deliberate security posture trade-off: slightly more maintenance overhead in exchange for tighter control over the dependency surface.

composer.lock — Recording the Patched Release

The lock file records the resolved package metadata. The key change is the version and Git reference for league/commonmark:

Before:

{
    "name": "league/commonmark",
    "version": "2.7.1",
    "source": {
        "type": "git",
        "url": "https://github.com/thephpleague/commonmark.git",
        "reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
    },
    "dist": {
        "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca",
        "reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
    }
}

After:

{
    "name": "league/commonmark",
    "version": "2.9.0",
    "source": {
        "type": "git",
        "url": "https://github.com/thephpleague/commonmark.git",
        "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529"
    },
    "dist": {
        "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
        "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529"
    }
}

The content-hash of the lock file also changed from a961e0ba61e216076bb4c0bd520eb32f to 3add01dd708f6be5dfd7f4293205f1d8, reflecting the updated dependency tree. Both the source reference and the dist URL now point to the 2.9.0 commit (5703d83ba3da3b2e356a5fedc848ed6d8ffb6529), which contains the upstream fix for the adjacent inline attribute block parsing logic.

Why This Fix Works

The league/commonmark 2.9.0 release patches the internal inline attribute block parser to correctly limit the number of consecutive attribute blocks it will process, and/or fixes the backtracking behavior so that processing time remains linear (or at worst polynomial with a low exponent) rather than exponential with respect to the number of adjacent blocks. The fix is contained entirely within the library — no application-level code changes are needed. Valid Markdown with a reasonable number of attribute blocks continues to render correctly; only the pathological "many adjacent blocks" case is bounded.


Key Takeaways

  • Adjacent {...} attribute blocks are the trigger — the vulnerability is not in Markdown generally, but specifically in the inline attribute block extension of league/commonmark. Any application using this extension with user input was exposed.
  • ^2.1 in composer.json was the enabling condition — the overly broad semver range allowed the vulnerable 2.7.1 to be installed and stay installed. Pinning to 2.9.0 closes this gap.
  • The composer.lock commit hash matters — the change from reference 10732241927d3971d28e7ea7b5712721fa2296ca (2.7.1) to 5703d83ba3da3b2e356a5fedc848ed6d8ffb6529 (2.9.0) is the concrete artifact of the fix; reviewing lock file diffs is a meaningful security activity.
  • DoS via parsing is cheap for attackers — a payload of a few hundred bytes can saturate a server. The asymmetry between attacker cost and defender cost makes this class of bug especially dangerous in public-facing applications.
  • composer audit in CI would have caught this — a single pipeline step would have flagged league/commonmark 2.7.1 as vulnerable before any deployment.

How Orbis AppSec Detected This

  • Source: User-supplied Markdown content submitted to any application endpoint that passes input to league/commonmark's parser.
  • Sink: The inline attribute block parser within league/commonmark 2.7.1, invoked whenever the CommonMark parser processes inline content containing {...} blocks.
  • Missing control: No bound on the number of consecutive inline attribute blocks processed in a single parse pass; no input size or complexity limit enforced before the parser is invoked.
  • CWE: CWE-400 — Uncontrolled Resource Consumption
  • Fix: composer.json version constraint for league/commonmark was changed from ^2.1 to the exact pin 2.9.0, and composer.lock was regenerated to record the patched release at Git reference 5703d83ba3da3b2e356a5fedc848ed6d8ffb6529.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

GHSA-g2gp-3wwq-f4ph is a high-severity reminder that parsing libraries are attack surface. The league/commonmark inline attribute block parser, when presented with a stream of adjacent {...} blocks, consumed unbounded resources — turning a few hundred bytes of Markdown into a server-killing payload. The fix is straightforward: upgrade to 2.9.0, pin the version, and add dependency scanning to CI so the next advisory doesn't linger in your lock file.

The broader lesson is that semver ranges like ^2.1 trade security predictability for convenience. For production dependencies that handle untrusted input — parsers, serializers, template engines — tighter version constraints and automated advisory scanning are not optional hardening; they are baseline hygiene.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #34

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.