---
title: Escape every value for its context
description: >-
  Escape every metafield, block setting, and configuration value for the exact
  context it renders in. Liquid doesn't escape output for you.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/security/storefront/escape-every-value-for-its-context
  md: >-
    https://shopify.dev/docs/apps/build/security/storefront/escape-every-value-for-its-context.md
---

# 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.

***

## Why

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:

```liquid
{%- 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](https://shopify.dev/docs/apps/build/security/backend/authenticate-every-request) controls what this snippet renders. CSP doesn't apply, because the script is rendered into the page rather than fetched from another domain.

***

## How

### Pick the filter for the context

| Where the value renders | What to use |
| - | - |
| HTML text, or an attribute value in quotes | `escape`. Always quote attribute values. |
| Inside a `<script>` element | `json`, 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>` element | Custom CSS that you validate when it's saved. See below. |
| A URL query parameter | `url_encode`. Validate the scheme of any full URL. |
| HTML that merchants need to write | HTML that you sanitize on your server with an allowlist sanitizer when it's saved. For rich text metafields, use `metafield_tag`. |

```liquid
{% 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`.

### Validate 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 merchants 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 merchant-supplied HTML and URLs, see [Render untrusted content as text](https://shopify.dev/docs/apps/build/security/backend/render-untrusted-content-as-text).

***
