diff --git a/.changeset/clever-pans-tap.md b/.changeset/clever-pans-tap.md
new file mode 100644
index 0000000000..0ddc24ef41
--- /dev/null
+++ b/.changeset/clever-pans-tap.md
@@ -0,0 +1,5 @@
+---
+'@shopify/hydrogen': patch
+---
+
+Fix XSS vulnerability
diff --git a/package-lock.json b/package-lock.json
index a76a9bc5c2..3f66b87f62 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30978,7 +30978,7 @@
},
"packages/cli": {
"name": "@shopify/cli-hydrogen",
- "version": "5.5.1",
+ "version": "5.5.2",
"license": "MIT",
"dependencies": {
"@ast-grep/napi": "0.11.0",
@@ -31873,7 +31873,7 @@
"dependencies": {
"@remix-run/react": "1.19.1",
"@shopify/cli": "3.49.2",
- "@shopify/cli-hydrogen": "^5.5.1",
+ "@shopify/cli-hydrogen": "^5.5.2",
"@shopify/hydrogen": "^2023.7.13",
"@shopify/remix-oxygen": "^1.1.8",
"@total-typescript/ts-reset": "^0.4.2",
@@ -31904,7 +31904,7 @@
"dependencies": {
"@remix-run/react": "1.19.1",
"@shopify/cli": "3.49.2",
- "@shopify/cli-hydrogen": "^5.5.1",
+ "@shopify/cli-hydrogen": "^5.5.2",
"@shopify/hydrogen": "^2023.7.13",
"@shopify/remix-oxygen": "^1.1.8",
"graphql": "^16.6.0",
@@ -42976,7 +42976,7 @@
"@remix-run/dev": "1.19.1",
"@remix-run/react": "1.19.1",
"@shopify/cli": "3.49.2",
- "@shopify/cli-hydrogen": "^5.5.1",
+ "@shopify/cli-hydrogen": "^5.5.2",
"@shopify/hydrogen": "^2023.7.13",
"@shopify/oxygen-workers-types": "^3.17.3",
"@shopify/prettier-config": "^1.1.2",
@@ -49857,7 +49857,7 @@
"@remix-run/eslint-config": "1.19.1",
"@remix-run/react": "1.19.1",
"@shopify/cli": "3.49.2",
- "@shopify/cli-hydrogen": "^5.5.1",
+ "@shopify/cli-hydrogen": "^5.5.2",
"@shopify/hydrogen": "^2023.7.13",
"@shopify/oxygen-workers-types": "^3.17.3",
"@shopify/prettier-config": "^1.1.2",
diff --git a/packages/hydrogen/src/seo/escape.ts b/packages/hydrogen/src/seo/escape.ts
new file mode 100644
index 0000000000..bfc8650414
--- /dev/null
+++ b/packages/hydrogen/src/seo/escape.ts
@@ -0,0 +1,15 @@
+// This is taken from remix: https://github.com/remix-run/remix/blob/main/packages/remix-server-runtime/markup.ts
+
+const ESCAPE_LOOKUP: {[match: string]: string} = {
+ '&': '\\u0026',
+ '>': '\\u003e',
+ '<': '\\u003c',
+ '\u2028': '\\u2028',
+ '\u2029': '\\u2029',
+};
+
+const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
+
+export function escapeHtml(html: string) {
+ return html.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
+}
diff --git a/packages/hydrogen/src/seo/generate-seo-tags.ts b/packages/hydrogen/src/seo/generate-seo-tags.ts
index 048a2882cb..70c951966c 100644
--- a/packages/hydrogen/src/seo/generate-seo-tags.ts
+++ b/packages/hydrogen/src/seo/generate-seo-tags.ts
@@ -1,6 +1,7 @@
import type {ComponentPropsWithoutRef} from 'react';
import type {Maybe} from '@shopify/hydrogen-react/storefront-api-types';
import type {Thing, WithContext} from 'schema-dts';
+import {escapeHtml} from './escape';
const ERROR_PREFIX = 'Error in SEO input: ';
@@ -500,7 +501,9 @@ export function generateSeoTags<
'script',
{
type: 'application/ld+json',
- children: JSON.stringify(block),
+ children: JSON.stringify(block, (k, value) => {
+ return typeof value === 'string' ? escapeHtml(value) : value;
+ }),
},
// @ts-expect-error
`json-ld-${block?.['@type'] || block?.name || index++}`,
diff --git a/packages/hydrogen/src/seo/seo.test.ts b/packages/hydrogen/src/seo/seo.test.ts
index 972e35da05..2eb52d9ee4 100644
--- a/packages/hydrogen/src/seo/seo.test.ts
+++ b/packages/hydrogen/src/seo/seo.test.ts
@@ -264,6 +264,35 @@ describe('seo', () => {
`);
});
+
+ it('escapes script content', async () => {
+ vi.mocked(useMatches).mockReturnValueOnce([
+ fillMatch({
+ data: {
+ seo: {
+ jsonLd: {
+ '@context': 'https://schema.org',
+ '@type': 'Organization',
+ name: 'Hydrogen Root',
+ description: '',
+ },
+ },
+ },
+ }),
+ ]);
+
+ const {asFragment} = render(createElement(Seo));
+
+ expect(asFragment()).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
});
function fillMatch(partial: Partial = {}) {