Skip to content

Fix stored XSS in product JSON-LD via unescaped JSON.stringify (CWE-79)#1527

Open
alanturing881 wants to merge 1 commit into
vercel:mainfrom
alanturing881:fix/json-ld-xss-script-tag-injection
Open

Fix stored XSS in product JSON-LD via unescaped JSON.stringify (CWE-79)#1527
alanturing881 wants to merge 1 commit into
vercel:mainfrom
alanturing881:fix/json-ld-xss-script-tag-injection

Conversation

@alanturing881

Copy link
Copy Markdown

Security Fix — Stored XSS in Product JSON-LD (CWE-79)

Summary

Product pages render a JSON-LD <script> block using dangerouslySetInnerHTML with raw JSON.stringify output. Because JSON.stringify does not escape < or >, a product title or description containing </script> terminates the JSON-LD block prematurely, allowing an attacker-controlled <script> tag to execute in the customer's browser.

This is a stored XSS affecting any customer who visits a product page with a malicious title, description, or image URL — injected via the Shopify admin (compromised credentials, rogue admin, API abuse).

Affected file

app/product/[handle]/page.tsx line 79–82

Root cause

// Vulnerable — JSON.stringify does NOT escape < or >
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(productJsonLd),
  }}
/>

A product title of </script><script>alert(1)</script> produces:

<script type="application/ld+json">{"name":"</script><script>alert(1)</script>"...}</script>

The browser terminates the JSON-LD block at the first </script>, then executes the injected script.

Fix

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(productJsonLd)
      .replace(/</g, "\\u003c")
      .replace(/>/g, "\\u003e"),
  }}
/>

< / > are valid JSON Unicode escapes that browsers and JSON parsers decode as </>, so structured-data consumers see the correct values — but no </script> sequence can appear in the raw HTML.

Impact

  • Type: Stored XSS — CWE-79
  • Trigger: Shopify admin sets malicious product title/description (compromised account, rogue employee, API token with write_products scope)
  • Consequence: Arbitrary JavaScript executes in every customer's browser on the product page — session cookie theft, payment skimming, phishing redirects

Verification

Confirmed via Node.js runtime — JSON.stringify does not escape angle brackets:

node -e "const o={name:'</script><script>alert(1)</script>'}; console.log(JSON.stringify(o))"
# Output: {"name":"</script><script>alert(1)</script>"}   ← unescaped
…aping (CWE-79)

JSON.stringify does not escape `<` or `>`, so a product title or description
containing `</script>` breaks out of the JSON-LD <script> block, allowing
an injected <script> tag to execute. Escape `<` and `>` as their Unicode
equivalents `<`/`>` — valid JSON that browsers parse correctly
but that cannot form a raw `</script>` sequence in HTML.

Co-Authored-By: iaohkut <thb2601@gmail.com>
@vercel

vercel Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Someone is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant