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..bc43d40135 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 d228837ef6..a8ba522b47 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -293,6 +293,11 @@ 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')) + } + // 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 ed66b4e8ae..2aea4c8437 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,7 @@ export function buildCookies(session: DevServerSession, ctx: Pick