From 0d6275a75cfb33c8e47aceb96979d6947aa4ac5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rmungandrk?= Date: Tue, 14 Jul 2026 09:56:02 +0700 Subject: [PATCH 1/3] fix: Escape unsafe JS characters in channel routes Updated dispatchChannelWebSocketRequest and dispatchChannelRequest to escape unsafe JavaScript code characters in routeKey. --- .../src/internal/nitro/host/channel-routes.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/eve/src/internal/nitro/host/channel-routes.ts b/packages/eve/src/internal/nitro/host/channel-routes.ts index ec02822d3..877fb13af 100644 --- a/packages/eve/src/internal/nitro/host/channel-routes.ts +++ b/packages/eve/src/internal/nitro/host/channel-routes.ts @@ -17,6 +17,24 @@ import type { PreparedApplicationHost } from "#internal/nitro/host/types.js"; // Must stay under `#nitro/virtual/` — the dev bundler's virtual plugin // freezes its resolveId filter at config creation, and that prefix is the +const UNSAFE_JS_CODE_CHAR_MAP: Record = { + "<": "\\u003C", + ">": "\\u003E", + "/": "\\u002F", + "\\": "\\\\", + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\0": "\\0", + "\u2028": "\\u2028", + "\u2029": "\\u2029", +}; + +function escapeUnsafeJsCodeChars(value: string): string { + return value.replace(/[<>\/\\\b\f\n\r\t\0\u2028\u2029]/g, (char) => UNSAFE_JS_CODE_CHAR_MAP[char] ?? char); +} // only pattern under which channel routes added while `eve dev` runs can // still resolve (see dev-live-virtual-modules.ts). const EVE_CHANNEL_VIRTUAL_ID_PREFIX = "#nitro/virtual/eve-channel/"; @@ -187,7 +205,7 @@ function addChannelVirtualHandler( `import { defineWebSocketHandler } from ${nitroModulePath};`, `import { dispatchChannelWebSocketRequest } from ${dispatchModulePath};`, `const config = ${JSON.stringify(input.artifactsConfig)};`, - `export default defineWebSocketHandler((event) => dispatchChannelWebSocketRequest(event, ${JSON.stringify(routeKey)}, config));`, + `export default defineWebSocketHandler((event) => dispatchChannelWebSocketRequest(event, ${escapeUnsafeJsCodeChars(JSON.stringify(routeKey))}, config));`, ].join("\n"); return; } @@ -215,12 +233,12 @@ function addChannelVirtualHandler( `import { dispatchChannelRequest } from ${dispatchModulePath};`, `const config = ${JSON.stringify(input.artifactsConfig)};`, input.cors === undefined - ? `export default (event) => dispatchChannelRequest(event, ${JSON.stringify(routeKey)}, config);` + ? `export default (event) => dispatchChannelRequest(event, ${escapeUnsafeJsCodeChars(JSON.stringify(routeKey))}, config);` : [ `export default (event) => {`, ` const corsResponse = handleCors(event, cors);`, ` if (corsResponse !== false) return corsResponse;`, - ` return dispatchChannelRequest(event, ${JSON.stringify(routeKey)}, config);`, + ` return dispatchChannelRequest(event, ${escapeUnsafeJsCodeChars(JSON.stringify(routeKey))}, config);`, `};`, ].join("\n"), ].join("\n"); From 5bbb5dba4d3fd51e70733a96b7f18345a4304288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rmungandrk?= Date: Tue, 14 Jul 2026 08:13:21 +0000 Subject: [PATCH 2/3] fix: includesWorkflowRoute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörmungandrk --- .../nitro/host/configure-nitro-routes.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts b/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts index ffe216806..849d31645 100644 --- a/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts +++ b/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts @@ -33,8 +33,23 @@ function includesApplicationRoutes(surface: NitroBuildSurface): boolean { return surface === "all" || surface === "app"; } -function includesWorkflowBundles(surface: NitroBuildSurface): boolean { - return includesWorkflowRoute(surface); +const INLINE_JS_UNSAFE_CHAR_MAP: Record = { + "<": "\\u003C", + ">": "\\u003E", + "/": "\\u002F", + "\\": "\\\\", + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\0": "\\0", + "\u2028": "\\u2028", + "\u2029": "\\u2029", +}; + +function escapeUnsafeCharsForInlineJs(value: string): string { + return value.replace(/[<>\/\\\b\f\n\r\t\0\u2028\u2029]/g, (char) => INLINE_JS_UNSAFE_CHAR_MAP[char] ?? char); } function includesWorkflowRoute(surface: NitroBuildSurface): boolean { @@ -204,6 +219,10 @@ function buildWorkflowFileHandlerSource(input: { lines.push(`import ${JSON.stringify(input.workflowWorldPluginImportSpecifier)};`); } + // NOTE: The generated handler intentionally uses top-level `await`. + // This requires the emitted module to stay ESM (for example `.mjs`) and be + // loaded/transpiled by tooling that supports top-level await. Changing the + // extension/loader pipeline to CommonJS or non-TLA environments will fail at load time. lines.push( `import { getWorld as __eveGetWorkflowWorld } from ${JSON.stringify(input.runtimeImportSpecifier)};`, "", @@ -226,7 +245,7 @@ function buildWorkflowFileHandlerSource(input: { ); } - lines.push("", "export default async ({ req }) => {", " return await POST(req);", "};", ""); + lines.push("", "export default async ({ req }) => {", " return POST(req);", "};", ""); return lines.join("\n"); } @@ -252,6 +271,7 @@ function addFrameworkVirtualHandler( ): void { const virtualId = `#eve-route${input.route}`; const modulePath = stringifyEsmImportSpecifier(input.modulePath); + const escapedArgs = escapeUnsafeCharsForInlineJs(input.args); nitro.options.handlers.push({ handler: virtualId, @@ -260,7 +280,7 @@ function addFrameworkVirtualHandler( }); nitro.options.virtual[virtualId] = [ `import { ${input.handlerExport} } from ${modulePath};`, - `export default async (event) => ${input.handlerExport}(${input.args}, event.req);`, + `export default async (event) => ${input.handlerExport}(${escapedArgs}, event.req);`, ].join("\n"); } From e54fcdf7a6af078241075bdd59dbb0242e5a2c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rmungandrk?= Date: Tue, 14 Jul 2026 15:42:20 +0700 Subject: [PATCH 3/3] Update packages/eve/src/internal/nitro/host/configure-nitro-routes.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- .../nitro/host/configure-nitro-routes.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts b/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts index 849d31645..212807249 100644 --- a/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts +++ b/packages/eve/src/internal/nitro/host/configure-nitro-routes.ts @@ -36,20 +36,20 @@ function includesApplicationRoutes(surface: NitroBuildSurface): boolean { const INLINE_JS_UNSAFE_CHAR_MAP: Record = { "<": "\\u003C", ">": "\\u003E", - "/": "\\u002F", - "\\": "\\\\", - "\b": "\\b", - "\f": "\\f", - "\n": "\\n", - "\r": "\\r", - "\t": "\\t", - "\0": "\\0", "\u2028": "\\u2028", "\u2029": "\\u2029", }; +/** + * Escapes ONLY the characters that {@link JSON.stringify} leaves intact but are + * unsafe to embed directly in inline JS/HTML source (`<`, `>`, and the U+2028 / + * U+2029 line separators). The input is expected to already be a valid JS + * string literal produced by `JSON.stringify`, so backslashes, quotes and + * control characters are already escaped — re-escaping them here would double + * the backslashes and corrupt values such as Windows filesystem paths. + */ function escapeUnsafeCharsForInlineJs(value: string): string { - return value.replace(/[<>\/\\\b\f\n\r\t\0\u2028\u2029]/g, (char) => INLINE_JS_UNSAFE_CHAR_MAP[char] ?? char); + return value.replace(/[<>\u2028\u2029]/g, (char) => INLINE_JS_UNSAFE_CHAR_MAP[char] ?? char); } function includesWorkflowRoute(surface: NitroBuildSurface): boolean {