Sylius / Sylius/CmsPlugin

Global html_entity_decode over rendered content elements inverts Twig's output escaping and corrupts attributes carrying entities

Open
#202 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
18
Forks
31
PR merge metrics
No merged PRs in 30d

Description

Version: sylius/cms-plugin v1.1.6, Sylius 2.2

Problem

ContentElementRendererStrategy::render() pipes the rendered HTML of every content element through html_entity_decode(..., ENT_QUOTES):

https://github.com/Sylius/CmsPlugin/blob/v1.1.6/src/Renderer/ContentElementRendererStrategy.php#L45

The decode exists to compensate the Twig auto-escaping of elements that emit pre-rendered HTML through a plain {{ variable }} (textarea, single_media's {{ media.renderedContent }}, multiple_media, pages_collection). For those elements the escape-then-decode round trip is lossless. For every other element it is not, and it breaks things in two ways.

1. Twig's output escaping is inverted for plain-text fields

This uses only the plugin's own files. Rendering the stock shop/content_element/elements/heading.html.twig and then applying the strategy's exact decode:

$twig = new \Twig\Environment(new \Twig\Loader\ArrayLoader([
    'heading' => file_get_contents('vendor/sylius/cms-plugin/templates/shop/content_element/elements/heading.html.twig'),
]));
$rendered = $twig->render('heading', [
    'content_element_base_class' => 'content-element',
    'heading_type' => 'h2',
    'heading_content' => 'Summer offer <script>alert(1)</script>',
]);
echo html_entity_decode($rendered, ENT_QUOTES);

Output:

<div class="content-element__heading mb-3">
    <h2>Summer offer <script>alert(1)</script></h2>
</div>

The template escaped correctly (&lt;script&gt;); the strategy un-escaped it. Any markup typed into a plain-text field (heading, and every other escaped variable in any element template) lands unescaped in the shop page. I am not framing this as a vulnerability per se, since the WYSIWYG textarea element already allows raw HTML by design for the same back-office users, but it does demonstrate that the global decode silently defeats Twig's escaping contract for all elements.

Reproduction on a stock install: create a page with a heading element whose text is Hello <em>world</em>, render it on the shop. The <em> renders as markup instead of text.

2. Attributes legitimately carrying entities are truncated

Nested HTML that is already safe when it reaches the strategy (rendered by {{ component(...) }}, included product templates, etc.) is decoded once too. Any attribute whose value legitimately contains &quot; is cut at the first decoded quote:

before: <div data-live-props-value="{&quot;product&quot;:8553}">
after:  <div data-live-props-value="{"product":8553}">

The browser closes the attribute after {. In practice: a products_grid / products_carousel element whose product template uses any Symfony UX Live Component produces dead components on the CMS page (add to cart, wishlist, etc.), because data-live-props-value is destroyed. This is how we found the bug. The same applies to a plain alt="14&quot; wheel" coming from a product name with a double quote.

Note: Twig/Runtime/RenderContentRuntime.php#L27 applies the same global decode to full-page content.

Expected behaviour

The strategy should emit the rendered HTML untouched. The four element templates that emit pre-rendered HTML can unescape locally (|raw on their own variable), which keeps the intended behaviour without touching any other element's output.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/Renderer/ContentElementRendererStrategy.php around the global html_entity_decode, then inspect Twig/Runtime/RenderContentRuntime.php and the textarea, single_media, multiple_media, and pages_collection element templates. Reproduce the heading escaping and data-live-props-value cases from the issue. Done means rendered HTML remains intact while those four pre-rendered HTML elements retain their intended output.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.