CVE case study
CVE-2026-55890: The Sibling Sink an Incomplete Fix Missed
A Grav case study in variant analysis: one Markdown media-attribute sink was blocked while a sibling style path remained writable.
This is a defensive case study of CVE-2026-55890, based on the coordinated public advisory. It explains the trust boundary, the failure mode, and the engineering fix without reproducing a weaponized exploit.
- Severity
- Medium · 4.8
- Scoring
- CVSS 3.1
- Weakness
- CWE-79
- Affected
- Grav 2.0.0-rc.8 and earlier in the advisory’s supported 2.0 line
- Remediation state
- Upgrade to 2.0.0-rc.9 or later
- Advisory published
- 17 Jun 2026
Official vectorCVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N
Why it matters
The earlier fix correctly rejected dangerous names passed through a generic media attribute() action. But Grav exposed a sibling style() action through the same Markdown media pipeline, and that method still accepted editor-controlled CSS.
This is a classic incomplete-fix pattern: the vulnerable behavior was defined too narrowly as one function instead of the underlying capability—untrusted content influencing security-sensitive browser attributes.
How I discovered it
I approached Grav as an incomplete-patch review. The earlier fix for CVE-2026-42841 added a deny-list to MediaObjectTrait::attribute(), and style was explicitly one of the names it rejected. That was my clue to search the same class and dispatch pipeline for every other way Markdown could set style data.
A sibling method, MediaObjectTrait::style(), was only a few lines away. Its own documentation showed the Markdown form ?style=…, and the method appended the supplied value without applying the new policy. I then traced the Markdown query key through processMediaActions(), where the key becomes a method name, and finally into parsedownElement(), where the accumulated value becomes the rendered image’s inline style.
The final check was the save-side detector. Its rules looked for HTML tags, event handlers, dangerous protocols, or inline-style syntax in raw HTML. The source was Markdown, so the dangerous browser behavior appeared only after rendering and bypassed those source-level patterns.
Trust boundary and root cause
Markdown image actions were mapped to methods on a media object. The dedicated style method appended its input to the rendered image’s inline style value without applying the safety policy introduced for the generic attribute method.
The boundary crossed when a content editor’s stored Markdown was rendered for a higher-privileged viewer. The editor was allowed to author content, but not to control an administrator’s interface or authenticated browsing context.
Source-to-sink trace
- 01Patch clue
MediaObjectTrait::attribute()The earlier fix explicitly rejects the
styleattribute name. - 02Sibling entry
MediaObjectTrait::style()The direct style helper accepts a Markdown-controlled value without the new validation.
- 03Dynamic dispatch
Excerpts::processMediaActions()A Markdown image query key becomes a method call, so
?style=…reaches the sibling helper. - 04Stored sink
MediaObjectTrait::parsedownElement()The accumulated value is emitted as the image’s inline
styleattribute for future viewers.
- 01Authorized content edit
A publisher stores a Markdown image using the supported media-action syntax.
- 02Sibling method dispatch
The action pipeline reaches
MediaObjectTrait::style(), outside the earlier attribute-name gate. - 03Unsanitized style value
The value is concatenated into the rendered image’s inline CSS.
- 04Privileged view
An administrator or reviewer loads the stored page and receives attacker-influenced UI styling.
Safe proof of concept
Prerequisites
- A local Grav 2.0.0-rc.8 lab and a benign one-pixel PNG.
- An editor role with page-edit permission but without super-administrator permission.
- A disposable page such as
/cve-style-lab.
Step-by-step reproduction
- Log in to the local admin panel as the restricted editor and upload the benign image to the disposable page.
- Save the Markdown below. It uses only CSS that makes the data flow visually obvious.
- Open the public page or preview. Inspect the generated image element and confirm that the stored style value is present.
- As a control, try the older generic
attribute[style,...]path on the same version. The earlier fix should reject that route while the siblingstyle()route still renders. - Upgrade the lab to 2.0.0-rc.9 and repeat. The unsafe positioning and stacking declarations should be removed or rejected.
Benign stored-style proof
# Local Grav style validation
Rendered evidence check
curl -fsS http://127.0.0.1/cve-style-lab \
| rg -o '<img[^>]+style="[^"]+"'
# Vulnerable result includes:
# position:fixed ... border:8px solid red ... z-index:9999 ... pointer-events:noneExpected evidence
- The editor’s Markdown saves successfully on the vulnerable release.
- The rendered
<img>contains the supplied positioning, border, and stacking declarations. - The red proof element is visible to a separate viewer, demonstrating a stored cross-role browser effect without executing JavaScript.
Use the already-patched generic attribute() action to attempt to set style. It should not create the attribute. The difference between that control and the sibling ?style= path isolates the incomplete fix.
On 2.0.0-rc.9, repeat the exact page save and inspect the output. Positioning, stacking, function calls, at-rules, markup, and quoting characters should fail closed, while conservative layout styles allowed by the patch continue to work.
Impact in context
The public advisory describes persistent UI redress, deceptive overlays, interference with administrative actions, and limited CSS-based data exposure. Exploitation requires a high-privilege content-authoring role and a separate user to view the page, which is reflected in the 4.8 score.
The issue is better understood as a stored cross-role browser injection than as permission to style one’s own content: the affected viewer and the author do not share the same trust level.
Remediation and verification
Upgrade the supported 2.0 line to Grav 2.0.0-rc.9 or later. The advisory also observed 1.7.52 as vulnerable and states that no 1.7 backport applies, so operators should not describe that line as fixed. The durable control is one centralized policy for every route that can create an HTML attribute or CSS value.
Regression tests should enumerate sibling actions, mixed casing, alternate parsing forms, and stored rendering paths. A fix review should search for equivalent sinks before the patch is considered complete.
Engineering lesson
Patch review should move from function names to invariants. Here the invariant is that content-author input cannot create browser-active attributes or unrestricted styles in a more-privileged user’s view.
Primary public references
- Vendor advisory GHSA-pmf8-g7c8-7v54
- Central CVE record
- Style-validation fix commit
- Related earlier advisory GHSA-r7fx-8g49-7hhr
Disclosure boundary: only already-public technical detail is included here. Unpublished validation material, private communications, secrets, and weaponized payloads remain excluded.