#19817·OrchardCore

Move HTML sanitization to render time and isolate Liquid authoring

Author: sebastienrosCreated Sep 1, 2026Updated Sep 3, 2026

Is your feature request related to a problem?

Orchard Core currently sanitizes some HTML while content is edited and some HTML after it is rendered. Edit-time sanitization mutates authored source, can corrupt Liquid expressions, and cannot protect HTML introduced later by Liquid, Markdown conversion, shortcodes, alternate rendering surfaces, imported or legacy records, or programmatic writes.

Liquid authoring is also not represented consistently as a privileged capability. LiquidPart and Templates can emit trusted output, while HTML and Markdown parts and fields can optionally execute Liquid. LiquidPart has no dedicated authoring permission, and there is no equivalent LiquidField for migrating Liquid-enabled fields.

Describe the solution you'd like

Adopt a consistent invariant across 3.x and 4.0:

  • Persist authored HTML, Markdown, and Liquid source without sanitizing it.
  • When SanitizeHtml is enabled, sanitize final HTML every time it is rendered.
  • Treat Liquid authoring as privileged executable-template authoring.
  • Preserve Liquid compatibility in 3.x, then remove Liquid execution from non-Liquid parts and fields in 4.0.
  • Never rewrite existing content during migration.

Orchard Core 3.x (release/3.0)

Introduce ManageLiquidTemplates

Add a security-critical permission in OrchardCore.Liquid. Grant it by default to Administrator and Editor, migrate existing Editor roles to preserve their current capability, and update the Editor role description to state that Editors can edit content and Liquid templates.

Require it for:

  • Editing LiquidPart.
  • Editing Templates and Admin Templates in addition to their existing permissions.
  • Template preview.
  • Enabling RenderLiquid on HTML or Markdown definitions.
  • Editing HTML or Markdown parts and fields whose definitions have RenderLiquid enabled.

Authorization must be enforced server-side, including applicable API mutation paths. If a protected component cannot be rejected independently, reject editing the containing content item rather than silently dropping a forged value.

LiquidPart must not gain SanitizeHtml: Liquid can generate HTML, JSON, XML, text, or other formats, so an HTML sanitizer cannot establish a general safety boundary. Editing executable Liquid always requires ManageLiquidTemplates.

Remove edit-time sanitization

Remove sanitization from UpdateAsync, import handlers, API import paths, and other persistence-time paths. Saving or importing content must retain the exact authored source. Liquid validation remains when RenderLiquid is enabled and runs against the original source.

SanitizeHtml will mean “sanitize rendered HTML,” not “sanitize input.”

Sanitize every rendered output

Use authoritative output pipelines across Detail, Summary, Preview, BodyAspect, GraphQL rendered-HTML fields, and other framework-provided rendered-output APIs:

HtmlBodyPart / HtmlField:
stored HTML -> Liquid -> shortcodes -> sanitizer -> output

MarkdownBodyPart / MarkdownField:
stored Markdown -> Liquid -> Markdown conversion -> shortcodes -> sanitizer -> output

SanitizeHtml = false remains the explicit trusted-output opt-out.

Secure HTML menu items

HtmlMenuItemPart public rendering bypasses its content display driver. Sanitize its HTML at the actual MenuItemLink rendering boundary without mutating the stored or shared in-memory item. Validate the final link URL against unsafe schemes at the same boundary.

Do not restore the obsolete import handler: embedded menu items are not reliably visited by root content-part import coordination, and import-time sanitization conflicts with the render-time-only invariant.

Secure source editors

Encode stored source for its embedding context in normal and hidden textareas, Monaco/WYSIWYG initialization, previews, inline JSON, and inline JavaScript. Preserve exact round-tripping while removing unsafe raw source embedding.

Tests and documentation

Cover ordinary edits, forged posts, APIs, recipe imports, existing unsafe records, Liquid output, shortcode ordering, all built-in rendering surfaces, menu HTML and URL schemes, permission grants and denials, both sanitizer settings, and lossless editor round-tripping. Document the permission model, rendering order, trusted opt-out, and source-preservation behavior.

Orchard Core 4.0 (main)

Add LiquidField

Create the field equivalent of LiquidPart before removing Liquid from HTML and Markdown fields. It needs Liquid validation and rendering, editor and display shapes, GraphQL support comparable to HtmlField, appropriate indexing behavior, safe source embedding, and unconditional ManageLiquidTemplates enforcement for editing.

Do not add SanitizeHtml to LiquidField or LiquidPart. Their output is not constrained to HTML; context-specific encoding remains the responsibility of the template and its consuming surface.

Remove Liquid from non-Liquid components

Remove RenderLiquid, Liquid validation, and Liquid execution from:

  • HtmlBodyPart
  • HtmlField
  • MarkdownBodyPart
  • MarkdownField

Remove the related settings UI, service dependencies, handlers, GraphQL processing, and obsolete compatibility migrations. Keep shortcodes.

The 4.0 pipelines become:

stored HTML -> shortcodes -> sanitizer -> output
stored Markdown -> Markdown conversion -> shortcodes -> sanitizer -> output

Liquid execution remains in LiquidPart, LiquidField, Templates, and Admin Templates, all guarded by ManageLiquidTemplates.

Preserve source and warn during migration

Do not rewrite, convert, or re-sanitize existing content. During definition migration, emit warning-level logs for each HTML or Markdown part/field definition that had RenderLiquid enabled. Include the affected content type, part, and field and direct administrators to migrate manually to LiquidPart or LiquidField.

Unmigrated Liquid syntax remains stored but is rendered as ordinary HTML/Markdown source rather than executed.

Keep render-time sanitization

Render-time sanitization remains the permanent security boundary. No edit, import, API, remote publishing, or programmatic persistence path sanitizes stored source.

Tests and documentation

Test LiquidField, permission enforcement, removal of embedded Liquid execution, warning logs, unchanged stored source, continued shortcode behavior, legacy unsafe records, and manual migration paths. Document placement, shapes, GraphQL compatibility, warning messages, permission behavior, and the guarantee that migrations do not alter stored content.

Describe alternatives you've considered

  • Import-time sanitization: rejected because it misses embedded menu items, legacy records, APIs and other writers, and can corrupt Liquid before it is rendered.
  • Edit-time sanitization: rejected because it mutates authored source and does not cover later transformations or alternate ingestion paths.
  • Adding SanitizeHtml to Liquid components: rejected because Liquid output is not necessarily HTML and may intentionally produce JSON, XML, or text.
  • Automatically migrating or re-sanitizing stored content: rejected because it risks irreversible data loss and contract changes. Definition-level warnings plus documented manual migration preserve source safely.