CVE case study

CVE-2026-55890: Grav's style() Method Bypassed the Earlier Fix

Grav blocked style through attribute(), but the sibling style() action still accepted editor-controlled CSS.

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.

How I found it

I reviewed the fix for CVE-2026-42841, which blocked style in MediaObjectTrait::attribute(), then searched the same class and dispatch path for other style setters.

A sibling method, MediaObjectTrait::style(), was only a few lines away. Its own documentation showed the Markdown form ?style=<value>, and the method appended the supplied value without applying the new policy. I traced the Markdown query key through processMediaActions(), where the key becomes a method name, and into parsedownElement(), where the accumulated value becomes the rendered image's inline style.

The save-side detector checked raw HTML for tags, event handlers, dangerous protocols, and inline styles. The stored source was Markdown, so the browser-active CSS appeared only after rendering and bypassed those source-level checks.

Root cause

The first patch covered one function, not every path that could set browser styles. The dedicated style method still appended its input to the rendered image’s inline CSS.

Markdown was stored as content and rendered as inline CSS for another viewer. An editor could therefore affect an administrator’s interface.

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=<value> reaches the sibling helper.

  4. 04
    Stored sinkMediaObjectTrait::parsedownElement()

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

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

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.

A content editor could store CSS that rendered in an administrator's page, creating a cross-role browser injection.

Fix and retest

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. Use one validation policy for every method that can emit 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

Review every path that produces HTML attributes or CSS, not only the function named in the first report.

References

Further reading

Evidence connected to this article.

Back to article start