diff --git a/.changeset/rich-swans-draw.md b/.changeset/rich-swans-draw.md new file mode 100644 index 0000000000..e6d65015a7 --- /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.test.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts index 5775b1d5f7..8d6c91c095 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,18 @@ describe('dev proxy', () => { expect(canProxyRequest(event)).toBeTruthy() }) }) + describe('proxyStorefrontRequest', () => { + test('should reject hostname mismatch and throw error for non-CDN paths (SSRF protection)', async () => { + const event = createH3Event('GET', '//evil.com/some-path') + await expect(proxyStorefrontRequest(event, ctx)).rejects.toThrow( + 'Request failed: Hostname mismatch. Expected host: my-store.myshopify.com. Resulting URL hostname: evil.com', + ) + }) + test('should reject hostname mismatch and throw error for CDN paths (SSRF protection)', async () => { + const event = createH3Event('GET', '/ext/cdn//evil.com/some-path') + await expect(proxyStorefrontRequest(event, ctx)).rejects.toThrow( + 'Request failed: Hostname mismatch. Expected host: cdn.shopify.com. Resulting URL hostname: evil.com', + ) + }) + }) }) diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.ts index d228837ef6..b2178b2c44 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -293,6 +293,13 @@ 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}`) + // Check that we aren't redirecting to external hosts + if (url.hostname !== host) { + return Promise.reject( + new Error(`Request failed: Hostname mismatch. Expected host: ${host}. Resulting URL hostname: ${url.hostname}`), + ) + } + // 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`