From 3c30c939a3443ce4e513882d48da4a8496611974 Mon Sep 17 00:00:00 2001 From: Josh Faigan Date: Tue, 20 Jan 2026 11:20:13 -0500 Subject: [PATCH 1/3] wip --- .../theme/src/cli/utilities/theme-environment/proxy.ts | 10 ++++++++++ .../utilities/theme-environment/storefront-renderer.ts | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.ts index d228837ef64..ddcc02b2042 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -293,6 +293,16 @@ export function proxyStorefrontRequest(event: H3Event, ctx: DevServerContext): P const host = event.path.startsWith(EXTENSION_CDN_PREFIX) ? 'cdn.shopify.com' : ctx.session.storeFqdn const url = new URL(path, `https://${host}`) + // DEBUG: SSRF vulnerability test logging + console.log('--- SSRF VULNERABILITY TEST LOG ---') + console.log('[PROXY.TS] Input path:', path) + console.log('[PROXY.TS] Expected host:', host) + console.log('[PROXY.TS] Resulting URL hostname:', url.hostname) + console.log('[PROXY.TS] Full URL:', url.href) + if (url.hostname !== host) { + console.log('[PROXY.TS] ⚠️ HOST MISMATCH DETECTED - POTENTIAL SSRF!') + } + // When a .css.liquid or .js.liquid file is requested but it doesn't exist in SFR, // it will be rendered with a query string like `assets/file.css?1234`. // For some reason, after refreshing, this rendered URL keeps the wrong `?1234` diff --git a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts index ed66b4e8ae0..63de9f1d409 100644 --- a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts +++ b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts @@ -117,6 +117,14 @@ export function buildCookies(session: DevServerSession, ctx: Pick Date: Thu, 22 Jan 2026 20:12:24 -0800 Subject: [PATCH 2/3] fix added, unit test written and proxy test suite passing --- .../cli/utilities/theme-environment/proxy.test.ts | 14 +++++++++++++- .../src/cli/utilities/theme-environment/proxy.ts | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts index 5775b1d5f76..bc43d40135c 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts @@ -1,4 +1,10 @@ -import {canProxyRequest, getProxyStorefrontHeaders, injectCdnProxy, patchRenderingResponse} from './proxy.js' +import { + canProxyRequest, + getProxyStorefrontHeaders, + injectCdnProxy, + patchRenderingResponse, + proxyStorefrontRequest, +} from './proxy.js' import {describe, test, expect} from 'vitest' import {createEvent} from 'h3' import {IncomingMessage, ServerResponse} from 'node:http' @@ -338,4 +344,10 @@ describe('dev proxy', () => { expect(canProxyRequest(event)).toBeTruthy() }) }) + describe('proxyStorefrontRequest', () => { + test('should throw an error when URL hostname does not match expected host (SSRF protection)', async () => { + const event = createH3Event('GET', '//evil.com/some-path') + await expect(proxyStorefrontRequest(event, ctx)).rejects.toThrow('Request failed: Hostname mismatch') + }) + }) }) diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.ts index ddcc02b2042..967e8809e0e 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -300,7 +300,7 @@ export function proxyStorefrontRequest(event: H3Event, ctx: DevServerContext): P console.log('[PROXY.TS] Resulting URL hostname:', url.hostname) console.log('[PROXY.TS] Full URL:', url.href) if (url.hostname !== host) { - console.log('[PROXY.TS] ⚠️ HOST MISMATCH DETECTED - POTENTIAL SSRF!') + return Promise.reject(new Error('Request failed: Hostname mismatch')) } // When a .css.liquid or .js.liquid file is requested but it doesn't exist in SFR, From d526b3254a5f93aa30ca61f8bd14a1cc249d3e80 Mon Sep 17 00:00:00 2001 From: FKauwe Date: Fri, 23 Jan 2026 13:03:12 -0800 Subject: [PATCH 3/3] remove console logs and add changelog summary --- .changeset/rich-swans-draw.md | 5 +++++ .../theme/src/cli/utilities/theme-environment/proxy.ts | 7 +------ .../cli/utilities/theme-environment/storefront-renderer.ts | 7 ------- 3 files changed, 6 insertions(+), 13 deletions(-) create mode 100644 .changeset/rich-swans-draw.md diff --git a/.changeset/rich-swans-draw.md b/.changeset/rich-swans-draw.md new file mode 100644 index 00000000000..e6d65015a79 --- /dev/null +++ b/.changeset/rich-swans-draw.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme': patch +--- + +Protect SSRF vulnerability in proxy requests when hosts don't match diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.ts index 967e8809e0e..a8ba522b479 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -293,12 +293,7 @@ export function proxyStorefrontRequest(event: H3Event, ctx: DevServerContext): P const host = event.path.startsWith(EXTENSION_CDN_PREFIX) ? 'cdn.shopify.com' : ctx.session.storeFqdn const url = new URL(path, `https://${host}`) - // DEBUG: SSRF vulnerability test logging - console.log('--- SSRF VULNERABILITY TEST LOG ---') - console.log('[PROXY.TS] Input path:', path) - console.log('[PROXY.TS] Expected host:', host) - console.log('[PROXY.TS] Resulting URL hostname:', url.hostname) - console.log('[PROXY.TS] Full URL:', url.href) + // Check that we aren't redirecting to external hosts if (url.hostname !== host) { return Promise.reject(new Error('Request failed: Hostname mismatch')) } diff --git a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts index 63de9f1d409..2aea4c84370 100644 --- a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts +++ b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts @@ -118,13 +118,6 @@ function buildStorefrontUrl(session: DevServerSession, {path, sectionId, appBloc const baseUrl = buildBaseStorefrontUrl(session) const url = `${baseUrl}${path}` - // DEBUG: SSRF vulnerability test logging - console.log('[STOREFRONT-RENDERER.TS] Input path:', path) - console.log('[STOREFRONT-RENDERER.TS] Base URL:', baseUrl) - console.log('[STOREFRONT-RENDERER.TS] Concatenated URL:', url) - const parsedUrl = new URL(url) - console.log('[STOREFRONT-RENDERER.TS] Parsed hostname:', parsedUrl.hostname) - const params = new URLSearchParams({ _fd: '0', pb: '0',