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
Copy link
Copy Markdown
Contributor

vercel Bot commented May 31, 2026

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant