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

  1. 01
    Patch clueMediaObjectTrait::attribute()

    The earlier fix explicitly rejects the style attribute name.

  2. 02
    Sibling entryMediaObjectTrait::style()

    The direct style helper accepts a Markdown-controlled value without the new validation.

  3. 03
    Dynamic dispatchExcerpts::processMediaActions()

    A Markdown image query key becomes a method call, so ?style=… reaches the sibling helper.

  4. 04
    Stored sinkMediaObjectTrait::parsedownElement()

    The accumulated value is emitted as the image’s inline style attribute for future viewers.

Stored style-injection pathThe diagram focuses on data flow; no reusable payload is needed to understand the flaw.
  1. 01
    Authorized content edit

    A publisher stores a Markdown image using the supported media-action syntax.

  2. 02
    Sibling method dispatch

    The action pipeline reaches MediaObjectTrait::style(), outside the earlier attribute-name gate.

  3. 03
    Unsanitized style value

    The value is concatenated into the rendered image’s inline CSS.

  4. 04
    Privileged 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

  1. Log in to the local admin panel as the restricted editor and upload the benign image to the disposable page.
  2. Save the Markdown below. It uses only CSS that makes the data flow visually obvious.
  3. Open the public page or preview. Inspect the generated image element and confirm that the stored style value is present.
  4. As a control, try the older generic attribute[style,...] path on the same version. The earlier fix should reject that route while the sibling style() route still renders.
  5. 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

![local proof](pixel.png?style=position:fixed;top:16px;left:16px;width:220px;height:80px;border:8px solid red;z-index:9999;pointer-events:none)

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:none

Expected 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.
Negative control

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.

Fixed-version re-test

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

Disclosure boundary: only already-public technical detail is included here. Unpublished validation material, private communications, secrets, and weaponized payloads remain excluded.