From 9fa7fdec5d3f69ea407cc3317a6af6448b3a812d Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Mon, 8 Jun 2026 00:22:43 +0000 Subject: [PATCH] [Security] Harden dev console assets middleware against path traversal Hardens `devConsoleAssetsMiddleware` by implementing an `isSubpath` check to ensure requested asset paths remain within the intended `rootDirectory`, preventing path traversal vulnerabilities. --- .../dev/extension/server/middlewares.test.ts | 27 +++++++++++++++++++ .../dev/extension/server/middlewares.ts | 7 ++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts b/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts index 206d63ed971..db6bfb7f592 100644 --- a/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts +++ b/packages/app/src/cli/services/dev/extension/server/middlewares.test.ts @@ -6,6 +6,7 @@ import { noCacheMiddleware, redirectToDevConsoleMiddleware, getExtensionPointMiddleware, + devConsoleAssetsMiddleware, } from './middlewares.js' import * as utilities from './utilities.js' import {GetExtensionsMiddlewareOptions} from './models.js' @@ -15,6 +16,7 @@ import {UIExtensionPayload} from '../payload/models.js' import {testUIExtension} from '../../../../models/app/app.test-data.js' import {AppEventWatcher} from '../../app-events/app-event-watcher.js' import {copyConfigKeyEntry} from '../../../build/steps/include-assets/copy-config-key-entry.js' +import * as fs from '@shopify/cli-kit/node/fs' import {describe, expect, vi, test} from 'vitest' import {inTemporaryDirectory, mkdir, touchFile, writeFile} from '@shopify/cli-kit/node/fs' import * as h3 from 'h3' @@ -691,6 +693,31 @@ describe('getExtensionPayloadMiddleware()', () => { }) }) +describe('devConsoleAssetsMiddleware()', () => { + test('returns 404 for path traversal attempts', async () => { + await inTemporaryDirectory(async (tmpDir: string) => { + vi.spyOn(utilities, 'sendError').mockImplementation(() => ({}) as any) + vi.spyOn(fs, 'findPathUp').mockResolvedValue(joinPath(tmpDir, 'assets')) + + await fs.mkdir(joinPath(tmpDir, 'assets')) + await writeFile(joinPath(tmpDir, 'secret.txt'), 'secret') + + const event = getMockEvent({ + params: { + assetPath: '../secret.txt', + }, + }) + + await devConsoleAssetsMiddleware(event) + + expect(utilities.sendError).toHaveBeenCalledWith(event, { + statusCode: 404, + statusMessage: 'Not Found', + }) + }) + }) +}) + describe('getExtensionPointMiddleware()', () => { test('returns a 404 if the extension is not found', async () => { vi.spyOn(utilities, 'sendError').mockImplementation(() => {}) diff --git a/packages/app/src/cli/services/dev/extension/server/middlewares.ts b/packages/app/src/cli/services/dev/extension/server/middlewares.ts index 1f1789662a3..c090288a0bb 100644 --- a/packages/app/src/cli/services/dev/extension/server/middlewares.ts +++ b/packages/app/src/cli/services/dev/extension/server/middlewares.ts @@ -146,8 +146,13 @@ export const devConsoleAssetsMiddleware = defineEventHandler(async (event) => { }) } + const candidate = resolvePath(joinPath(rootDirectory, assetPath)) + if (!isSubpath(rootDirectory, candidate)) { + return sendError(event, {statusCode: 404, statusMessage: 'Not Found'}) + } + return fileServerMiddleware(event, { - filePath: joinPath(rootDirectory, assetPath), + filePath: candidate, }) })