From d94e422c1d41965eb90714220ccbe0cc09b5d2f9 Mon Sep 17 00:00:00 2001 From: caohuilin Date: Thu, 18 Dec 2025 21:18:51 +0800 Subject: [PATCH] fix: escape special characters in static file routes to prevent regex syntax errors --- .changeset/rotten-wolves-pull.md | 8 +++++ .../server/core/src/plugins/render/index.ts | 26 +++++++++++++-- pnpm-lock.yaml | 32 +++++++++---------- .../server-prod/config/public/test(bug.txt | 1 + .../server-prod/tests/index.test.ts | 8 +++++ 5 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 .changeset/rotten-wolves-pull.md create mode 100644 tests/integration/server-prod/config/public/test(bug.txt diff --git a/.changeset/rotten-wolves-pull.md b/.changeset/rotten-wolves-pull.md new file mode 100644 index 000000000000..536680e662db --- /dev/null +++ b/.changeset/rotten-wolves-pull.md @@ -0,0 +1,8 @@ +--- +'@modern-js/server-core': patch +--- + +fix: escape special characters in static file routes to prevent regex syntax errors + +fix: 转义静态文件路由中的特殊字符,防止正则表达式语法错误 + diff --git a/packages/server/core/src/plugins/render/index.ts b/packages/server/core/src/plugins/render/index.ts index 55aa61af8955..5b3e0c15f612 100644 --- a/packages/server/core/src/plugins/render/index.ts +++ b/packages/server/core/src/plugins/render/index.ts @@ -15,6 +15,18 @@ import { requestLatencyMiddleware } from '../monitors'; export * from './inject'; +const DYNAMIC_ROUTE_REG = /\/:./; + +/** + * Escape special regex characters in a path string. + * This is needed because Hono's router converts paths to regex patterns, + * and special characters like parentheses need to be escaped. + */ +function escapeRegexSpecialChars(path: string): string { + // Escape special regex characters: ( ) [ ] { } * + ? . ^ $ | \ + return path.replace(/[()[\]{}*+?.^$|\\]/g, '\\$&'); +} + export const renderPlugin = (): ServerPluginLegacy => ({ name: '@modern-js/plugin-render', @@ -47,9 +59,17 @@ export const renderPlugin = (): ServerPluginLegacy => ({ for (const route of pageRoutes) { const { urlPath: originUrlPath, entryName = MAIN_ENTRY_NAME } = route; - const urlPath = originUrlPath.endsWith('/') - ? `${originUrlPath}*` - : `${originUrlPath}/*`; + const isDynamic = DYNAMIC_ROUTE_REG.test(originUrlPath); + + // For static routes, escape special regex characters to prevent regex syntax errors + // For dynamic routes, keep as-is since they contain route parameters + const escapedPath = isDynamic + ? originUrlPath + : escapeRegexSpecialChars(originUrlPath); + + const urlPath = escapedPath.endsWith('/') + ? `${escapedPath}*` + : `${escapedPath}/*`; // Hook middleware will handle stream as string and then handle it as stream, which will cause the performance problem // TODO: Hook middleware will be deprecated in next version diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89834502223f..fba2328fe478 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4148,10 +4148,10 @@ importers: version: link:../../toolkit/utils '@storybook/react': specifier: ~7.6.1 - version: 7.6.20(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) + version: 7.6.20(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) storybook: specifier: ~7.6.1 - version: 7.6.20(bufferutil@4.0.8)(utf-8-validate@5.0.10) + version: 7.6.20(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) devDependencies: '@storybook/types': specifier: ~7.6.12 @@ -33072,7 +33072,7 @@ snapshots: '@storybook/components': 7.6.20(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/core-events': 7.6.20 '@storybook/csf': 0.1.11 - '@storybook/docs-tools': 7.6.20 + '@storybook/docs-tools': 7.6.20(encoding@0.1.13) '@storybook/global': 5.0.0 '@storybook/manager-api': 7.6.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/preview-api': 7.6.20 @@ -33098,7 +33098,7 @@ snapshots: - encoding - supports-color - '@storybook/builder-manager@7.6.20': + '@storybook/builder-manager@7.6.20(encoding@0.1.13)': dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 '@storybook/core-common': 7.6.20(encoding@0.1.13) @@ -33129,7 +33129,7 @@ snapshots: telejson: 7.2.0 tiny-invariant: 1.3.3 - '@storybook/cli@7.6.20(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@storybook/cli@7.6.20(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@babel/core': 7.26.10 '@babel/preset-env': 7.26.9(@babel/core@7.26.10) @@ -33138,10 +33138,10 @@ snapshots: '@storybook/codemod': 7.6.20 '@storybook/core-common': 7.6.20(encoding@0.1.13) '@storybook/core-events': 7.6.20 - '@storybook/core-server': 7.6.20(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@storybook/core-server': 7.6.20(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@storybook/csf-tools': 7.6.20 '@storybook/node-logger': 7.6.20 - '@storybook/telemetry': 7.6.20 + '@storybook/telemetry': 7.6.20(encoding@0.1.13) '@storybook/types': 7.6.20 '@types/semver': 7.7.0 '@yarnpkg/fslib': 2.10.3 @@ -33274,11 +33274,11 @@ snapshots: dependencies: ts-dedent: 2.2.0 - '@storybook/core-server@7.6.20(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@storybook/core-server@7.6.20(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.6.20 + '@storybook/builder-manager': 7.6.20(encoding@0.1.13) '@storybook/channels': 7.6.20 '@storybook/core-common': 7.6.20(encoding@0.1.13) '@storybook/core-events': 7.6.20 @@ -33289,7 +33289,7 @@ snapshots: '@storybook/manager': 7.6.20 '@storybook/node-logger': 7.6.20 '@storybook/preview-api': 7.6.20 - '@storybook/telemetry': 7.6.20 + '@storybook/telemetry': 7.6.20(encoding@0.1.13) '@storybook/types': 7.6.20 '@types/detect-port': 1.3.5 '@types/node': 18.19.74 @@ -33350,7 +33350,7 @@ snapshots: '@storybook/docs-mdx@0.1.0': {} - '@storybook/docs-tools@7.6.20': + '@storybook/docs-tools@7.6.20(encoding@0.1.13)': dependencies: '@storybook/core-common': 7.6.20(encoding@0.1.13) '@storybook/preview-api': 7.6.20 @@ -33444,11 +33444,11 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@storybook/react@7.6.20(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': + '@storybook/react@7.6.20(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3)': dependencies: '@storybook/client-logger': 7.6.20 '@storybook/core-client': 7.6.20 - '@storybook/docs-tools': 7.6.20 + '@storybook/docs-tools': 7.6.20(encoding@0.1.13) '@storybook/global': 5.0.0 '@storybook/preview-api': 7.6.20 '@storybook/react-dom-shim': 7.6.20(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -33481,7 +33481,7 @@ snapshots: memoizerific: 1.11.3 qs: 6.14.0 - '@storybook/telemetry@7.6.20': + '@storybook/telemetry@7.6.20(encoding@0.1.13)': dependencies: '@storybook/client-logger': 7.6.20 '@storybook/core-common': 7.6.20(encoding@0.1.13) @@ -46501,9 +46501,9 @@ snapshots: store2@2.14.3: {} - storybook@7.6.20(bufferutil@4.0.8)(utf-8-validate@5.0.10): + storybook@7.6.20(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): dependencies: - '@storybook/cli': 7.6.20(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@storybook/cli': 7.6.20(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - encoding diff --git a/tests/integration/server-prod/config/public/test(bug.txt b/tests/integration/server-prod/config/public/test(bug.txt new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/tests/integration/server-prod/config/public/test(bug.txt @@ -0,0 +1 @@ +test diff --git a/tests/integration/server-prod/tests/index.test.ts b/tests/integration/server-prod/tests/index.test.ts index 30348c6cc80c..08a2ba56f16d 100644 --- a/tests/integration/server-prod/tests/index.test.ts +++ b/tests/integration/server-prod/tests/index.test.ts @@ -89,4 +89,12 @@ describe('test basic usage', () => { expect(status).toBe(successStatus); expect(headers['content-type']).toBe('image/png'); }); + + test(`should serve static file with special characters in filename`, async () => { + const { status, data } = await axios.get( + `http://localhost:${appPort}/test(bug.txt`, + ); + expect(status).toBe(successStatus); + expect(data.trim()).toBe('test'); + }); });