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.1 → 2.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: pinleague/commonmarkto2.9.0incomposer.jsonand regeneratecomposer.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 to2.9.0is the most conservative choice: it ensures reproducibility and prevents any future2.xrelease (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.
Prevention & Best Practices
1. Keep Dependency Constraints Tight
The original constraint ^2.1 was too permissive. It allowed the project to silently run on any vulnerable 2.x release. Best practice for production PHP applications:
- Use exact pins (
2.9.0) or narrow ranges (>=2.9.0 <2.10.0) for security-sensitive dependencies. - Commit
composer.lockto version control and treat changes to it as security-relevant events. - Run
composer audit(available since Composer 2.4) in CI to catch known-vulnerable packages automatically.
2. Scan Dependencies in CI/CD
Tools that would have caught this before it reached production:
- Trivy — detected this exact issue (as noted in the PR, scanner: trivy).
composer audit— built into Composer, checks against the PHP Security Advisories Database.- Dependabot or Renovate — automated PRs when new versions with security fixes are released.
- Snyk or FOSSA — commercial SCA tools with broader advisory databases.
Add at least one of these to your CI pipeline so vulnerable dependencies are flagged before they reach staging or production.
3. Never Trust User-Supplied Markdown Without Rate Limiting
Even with a patched parser, rendering user-supplied Markdown is computationally non-trivial. Apply defense in depth:
- Rate-limit Markdown rendering endpoints.
- Set PHP
max_execution_timeto a low value for rendering workers. - Limit input size — reject Markdown payloads above a reasonable byte threshold before they reach the parser.
- Sandbox rendering — consider rendering Markdown in a separate process or queue worker so a DoS attack cannot take down the main application process pool.
4. Understand the Attack Surface of Parsing Libraries
Parsing libraries — Markdown, XML, JSON, CSV — are a common source of DoS vulnerabilities because they must handle adversarial input by design. When evaluating a parsing library:
- Check its security advisory history.
- Look for evidence of fuzzing or adversarial testing in the project.
- Prefer libraries that have explicit policies on input size limits.
5. Relevant Standards
- CWE-400: Uncontrolled Resource Consumption — the root CWE for this class of vulnerability.
- OWASP: Denial of Service Cheat Sheet — practical mitigations for DoS in web applications.
- OWASP: Input Validation Cheat Sheet — general guidance on validating and bounding untrusted input.
Key Takeaways
- Adjacent
{...}attribute blocks are the trigger — the vulnerability is not in Markdown generally, but specifically in the inline attribute block extension ofleague/commonmark. Any application using this extension with user input was exposed. ^2.1incomposer.jsonwas the enabling condition — the overly broad semver range allowed the vulnerable2.7.1to be installed and stay installed. Pinning to2.9.0closes this gap.- The
composer.lockcommit hash matters — the change from reference10732241927d3971d28e7ea7b5712721fa2296ca(2.7.1) to5703d83ba3da3b2e356a5fedc848ed6d8ffb6529(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 auditin CI would have caught this — a single pipeline step would have flaggedleague/commonmark 2.7.1as 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/commonmark2.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.jsonversion constraint forleague/commonmarkwas changed from^2.1to the exact pin2.9.0, andcomposer.lockwas regenerated to record the patched release at Git reference5703d83ba3da3b2e356a5fedc848ed6d8ffb6529.
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.