Global html_entity_decode over rendered content elements inverts Twig's output escaping and corrupts attributes carrying entities
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 (<script>); 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 " is cut at the first decoded quote:
before: <div data-live-props-value="{"product":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" 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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