Skip to main content

Escape every value for its context

Escape every metafield, block setting, and configuration value for the exact context it renders in. Liquid doesn't escape output for you: {{ product.title }} emits raw markup.

The context is where the value lands in the page: HTML text, an attribute, a <script> or <style> element, or a URL.


A value that lands in a storefront page as markup or code, rather than as text, runs for every buyer who visits the store. A value isn't safe because it comes from a Shopify database. Consider this snippet:

{%- assign cfg = app.metafields.my_app.config.value -%}
<script>{{ cfg.custom_js }}</script>
<div style="{{ cfg.custom_css }}"></div>

Whoever can write that metafield controls the script that runs for every buyer. App-data metafields, which Liquid reads through app.metafields, can be written only by your app, but your app writes whatever its settings page or API accepts. Anyone who reaches that page or API through a compromised account or an unauthenticated endpoint controls what this snippet renders. CSP doesn't apply, because the script is rendered into the page rather than fetched from another domain.


Anchor to Pick the filter for the contextPick the filter for the context

Where the value rendersWhat to use
HTML text, or an attribute value in quotesescape. Always quote attribute values.
Inside a <script> elementjson, without adding quotes around the output. json outputs a JavaScript value and encodes <, >, and &, so the value can't close the <script> element.
Inside a <style> elementCustom CSS that you validate when it's saved. See below.
A URL query parameterurl_encode. Validate the scheme of any full URL.
HTML that app users need to writeHTML that you sanitize on your server with an allowlist sanitizer when it's saved. For rich text metafields, use metafield_tag.
{% comment %} HTML body or attribute {% endcomment %}
{{ product.title | escape }}

{% comment %} Inside a <script> tag {% endcomment %}
<script>
var title = {{ product.title | json }};
</script>

{% comment %} Inside a URL parameter {% endcomment %}
<a href="/search?q={{ query | url_encode }}">

escape is the wrong filter inside <script>, because HTML escaping doesn't stop a value from breaking out of a JavaScript string. Use json.

Anchor to Validate custom CSS when it's savedValidate custom CSS when it's saved

No Liquid filter makes custom CSS safe inside a <style> element. escape encodes < so that a value can't close the element, but it also breaks valid CSS that contains > or quotes, and it doesn't limit what the CSS does. If app users can add custom CSS, then validate it on your server when it's saved:

  • Reject values that contain <, or replace each < with the CSS escape \3c , so the value can't close the <style> element.
  • Reject @import, and allow url() values only for hosts that you expect.
  • Render the result only inside a <style> element, never in a style attribute or any other context.

For the JavaScript side of this practice, including the sinks to search for and how to handle HTML and URLs that app users supply, see Render untrusted content as text.


Was this page helpful?