Overview
This bug was introduced in #3467959: SDC and code component props should be able to receive HTML, editable in formatted text fields+widgets.
I have a basic text component. The schema invokes CKEditor:
props:
type: object
properties:
text:
title: Text
type: string
contentMediaType: text/html
x-formatting-context: block
However when I output this as {{ text }}, I get escaped HTML. I can fix this by doing {{ text|raw }}, but this isn't in the examples, and obviously has security implications. I'd also argue, this negatively affects the developer experience.
We had discussion in Slack at https://drupal.slack.com/archives/C072JMEPUS1/p1759666595931849. One note is that
My concern is that this text component could be used in different page builders outside of Canvas. And the other page builders might not use CKEditor to filter.
This could result in a XSS vulnerability.
Proposed resolution
Output as `markup` so the text doesn't get double escaped.
| Comment | File | Size | Author |
|---|---|---|---|
| #33 | Screenshot 2025-10-10 at 7.51.20 PM.png | 666.16 KB | wim leers |
| #19 | image.png | 92.84 KB | wim leers |
| #19 | Screenshot 2025-10-09 at 11.15.16 AM.png | 890.9 KB | wim leers |
| #17 | Screenshot 2025-10-09 at 10.06.48 AM.png | 490.65 KB | wim leers |
| #17 | Screenshot 2025-10-09 at 10.06.08 AM.png | 398.31 KB | wim leers |
Issue fork canvas-3550334
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
effulgentsia commentedWe definitely don't want people to use the
|rawfilter in Twig to work around this, so tagging as an RC blocker. @larowlan and @Wim Leers have some prior art from a different issue for how to fix this, so assigning to @larowlan because his workday starts sooner. If he can't get to it, we'll make sure that someone else does early this week.Comment #5
larowlanMoved the private MR to this issue
Comment #6
larowlanCredited @shitalb who also asked about this when the issue was private and was added there.
Comment #7
effulgentsia commentedThanks! Looks great so far, but needs an upgrade path (post_update function?) for existing Component config entities (anywhere else?) unless those are automatically regenerated with this new expression on cache clear.
Comment #8
larowlanThey will autogenerate but I _think_ that will create a new component version, so any content pointing at the old version will likely get double escaped. We might want to do an update hook to update the version of any content pointing to that version.
Comment #9
edwardsay commentedThe patch from the MR doesn't resolve the issue. Values are still displayed escaped if no |raw is used.
Comment #10
wim leers+1
@edwardsay: It will work only for new component instances. To confirm this: 1) apply patch, 2) drush cr, 3) create component instance, 4) observe that the new component instance works as expected, and pre-existing component instances continue to NOT work as expected.
Comment #11
edwardsay commentedYeah, I tried it. But it doesn't work even for new components. I'm gonna continue debugging this.

Comment #12
edwardsay commentedOkay, looks like I'm onto something.
web/core/modules/text/src/TextProcessed.php line 59
$item->format is empty for SDC components. So, Plain Text is used.
I'll continue later, don't have time now.
Comment #13
wim leersHuh! That shouldn't be the case! It should be using the
canvas_html_blocktext format:—
\Drupal\canvas\JsonSchemaInterpreter\JsonSchemaType::computeStorablePropShape()Comment #14
wim leersDebugged. Tests are failing for roughly the same reason as #12.
Guess why? Yet another broken
::generateSampleValue()in core 😅—
\Drupal\text\Plugin\Field\FieldType\TextItemBase::generateSampleValue()👆
formatis always set to the fallback format, instead of respecting theallowed_formatssetting 🙄Comment #17
wim leersThe screenshots in #11 match the failure in
prop-types.cy.js. IOW: they predicted test failure.If we look at Canvas' test-only

all-propsSDC, we see the same:I bet this is related to Drupal’s “safe markup” concept — SDCs expect pure strings (to conform to JSON schema), but that means Drupal re-escapes it. Dug in and … at least the SDC subsystem is so far not getting in the way: despite receiving

FilteredMarkupobjects, it does not fail SDC'sComponentValidator:And it even makes it all the way to the final per-SDC render array:

Which tells me the problem is deeper. 😅
Zero issues against the SDC subsystem for
FilteredMarkup, too: https://www.drupal.org/project/issues/drupal?text=filteredmarkup&status=...Comment #18
wim leersFilteredMarkupalso survives (i.e. is not downcast to juststringwhich was my original hypothesis):\Drupal\Core\Render\Element\ComponentElement::preRenderComponent()\Drupal\Core\Render\Element\ComponentElement::generateComponentTemplate()… which means my hypothesis was wrong.
The root cause:
\Drupal\text\Plugin\Field\FieldType\TextItemBase::applyDefaultValue()in core is wildly broken:That
@tododates back to at least 2013, probably long before (!!!). It should've been solved back when #784672: Allow text field to enforce a specific text format landed.Comment #19
wim leersFix pushed. Works fine locally now.
Tested with SDC:

@jessebaker tested with code components:
We discussed this internally yesterday; and the conclusion between @effulgentsia and I was:
So: will do exactly that.
Comment #20
wim leersNeeds 2 upstream core issues fixed:
formatvalue based on field instance'sallowed_formatssetting:CoreBugFixTextItemBaseDefaultValueTrait::generateSampleValue():CoreBugFixTextItemBaseGenerateSampleValueTraitComment #21
wim leersI'd like @penyaskito to do a final review pass 😇
Comment #22
wim leersFunny, sad and impressive all at once: one critical security bug in Canvas, the fix in Canvas is trivial (the update path won't be as trivial), but … it revealed TWO core bugs! 🤯
(Those required orders of magnitude more work, ironically!)
I should've spotted this bug in #3467959: SDC and code component props should be able to receive HTML, editable in formatted text fields+widgets 😬🫣
Comment #23
penyaskitoComment #25
wim leersMerged! Back to for update path.
And probably anything with an update path also needs a change record 😇
Comment #26
wim leersMulti-tasking and merging an MR == bad idea. I broke HEAD 🫣
Comment #29
wim leersHEAD is green again ✅
Comment #30
effulgentsia commentedYay that the fix itself got in for beta2! The update path doesn't need to block RC. In the meantime, people can manually edit and re-save their existing content that uses HTML props that are getting double escaped. Though since we're already past beta, it would be courteous for us to provide the update path relatively soon and certainly before a stable 1.0 release.
Comment #31
wim leersNo, they can't.
They'd need to recreate the component instances.
Only once #3463996: [META] [PP-1] When the field type, storage/instance settings, widget, expression or requiredness for an SDC/code component prop changes, the Content Creator must be able to upgrade is done, what you wrote will be true. So … to be on the safe side, restoring the tag.
Comment #32
effulgentsia commentedI discussed this with @wim leers and @lauriii. Ideally, since beta1 marked the point where we started promising to not break existing site data, we'd get this update path in before RC1. However, this will be the first Canvas update path that requires updating content entities, so it might take some time to write it and make sure it works properly, and DrupalCon is starting next week. Meanwhile, the lack of this update path doesn't result in a broken site per se, just some component instances (ones with HTML props) continuing to be double-escaped and needing to be replaced with new instances of the same component, copy the values from the old instance to the new one, and delete the old one, in order to fix the double escaping. Definitely annoying, but beta1 hasn't been out that long, so not that many sites affected by it, and for the sites that are, it should only require a few minutes of manual effort. Therefore, downgrading to an RC target, meaning a high priority thing to work on after we tag RC if it doesn't make it in before.
Comment #33
wim leersOutline of how this update path can/should work:
Componentconfig entities which have >=1 version with >=1 prop whoseexpressionis eitherℹ︎text_long␟processedorℹ︎text␟processed. Why these 2 expressions? They're the ones specified/hardcoded in\Drupal\canvas\JsonSchemaInterpreter\JsonSchemaType::computeStorablePropShape(), which is what https://git.drupalcode.org/project/canvas/-/merge_requests/194 fixed.All affected Component versions must be updated. @penyaskito and I just did something like that in #3532514: Gracefully handle components in active development: ensure great DX!
Componentconfig entities that were updated, B) the list of updated Component versions, C) the new active (latest) version of each of those Components.Use that tracked information to call\Drupal\canvas\Audit\ComponentAudit::getContentRevisionIdsUsingComponentIds(array $component_ids, array $version_ids)→ this tells us which content revisions need updating.However, things are simpler: because we know that no stored data needs to change, only
Componentversions, AND we know that only thePagecontent entity type can have a component tree field today, we can instead do something like:\Drupal\canvas\Audit\ComponentAudit::getConfigEntityDependenciesUsingComponent(), which unfortunately does not yet support filtering by Component version.You'll see that many of these things were actually built for
\Drupal\Tests\canvas\Kernel\Plugin\Canvas\ComponentSource\ComponentInputsEvolutionTest, which was part of #3523841: Versioned Component config entities (SDC, JS: prop_field_definitions, block: default_setting, all: slots for fallback) + component instances refer to versions ⇒ less data to store per XB field row.Comment #34
wim leers@penyaskito pointed out a weakness in my proposal: for content entities, my proposal assumes that all inputs are collapsed. Which is what #3538487: Don't allow passing uncollapsed inputs if using default expression fixed and provided an update path for.
But then again, the XB → Canvas rename (~2 months after that) means that anybody out there realistically has been running with that all along.
But, it'd still be safer to bring back that update path. Even if untested. (Because we can't really test it anymore, since our reference is now not XB 0.7.x, but Canvas alpha1… unless we fake it of course.)
Comment #37
penyaskitoComment #40
wim leersThe update path is present and has test coverage. Nice work, @penyaskito!
We dug into the depths of this MR and all it relates to. 99% of what we discussed is captured as MR comments.
High-level conclusions:
Componentversion became which new version, and then updating all existingComponentinstances that used the old version to the new version is … nice, but A) unnecessary (because the update path updates the old versions' expression!), B) overkill/out-of-scope, because that's literally what #3463996: [META] [PP-1] When the field type, storage/instance settings, widget, expression or requiredness for an SDC/code component prop changes, the Content Creator must be able to upgrade is about. Let's fix it holistically there, rather than paritally here.StaticPropSources —DynamicPropSources would've been correct already. Unless somebody manually constructed the expressions inDynamicPropSources in recipes/content's DB entries/exported config. But if that happened, you're on your own already.1+2 together means much of the update logic + test coverage becomes obsolete: the update path only needs to handle
Componentconfig entities, and doesn't need to track versions 👍Comment #41
penyaskitoThis is ready to go.
The only blocker is properly clarify why some updates run multiple times, and why the number of component versions is different between the sdc and the js examples.
Comment #42
nagwani commentedComment #43
larowlanA comment from @mglaman on #3547579: Introduce a new cache tags aware prop shape repository, so changes affecting prop shape calculation can force the re-invoke of hook_canvas_storable_shape_prop_alter made me realise the 2 vs 3 versions in the update test is actually a symptom of a bigger issue - so I opened #3556327: Don't save `Component` config entities unnecessarily
Comment #44
larowlanI postponed #3556327: Don't save `Component` config entities unnecessarily on this because the new tests here will give us coverage for it.
Comment #45
wim leersComponentsThis is ready! 🚀 Thanks for the epic work here, @penyaskito!
Comment #47
wim leersAssigning to @penyaskito for the last 3.
I chimed in with proposed next steps at #3556327-6: Don't save `Component` config entities unnecessarily.
Comment #48
penyaskitoCreated #3556506: Respect allowed_formats settings in \Drupal\text\Plugin\Field\FieldType\TextItemBase::applyDefaultValue and #3556508: Respect allowed_formats settings in \Drupal\text\Plugin\Field\FieldType\TextItemBase::generateSampleValue on Core's queue per #20.
Updated change record at https://www.drupal.org/node/3556442
Comment #50
penyaskitoComment #51
wim leersThanks for those, and for the published CR: https://www.drupal.org/node/3556442 :)