Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions packages/eve/src/internal/nitro/host/channel-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
"<": "\\u003C",
">": "\\u003E",
"/": "\\u002F",
"\\": "\\\\",
"\b": "\\b",
"\f": "\\f",
"\n": "\\n",
"\r": "\\r",
"\t": "\\t",
"\0": "\\0",
"\u2028": "\\u2028",
"\u2029": "\\u2029",
};

function escapeUnsafeJsCodeChars(value: string): string {
Comment thread
odaysec marked this conversation as resolved.
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/";
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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");
Expand Down
28 changes: 24 additions & 4 deletions packages/eve/src/internal/nitro/host/configure-nitro-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
"<": "\\u003C",
">": "\\u003E",
"\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(/[<>\u2028\u2029]/g, (char) => INLINE_JS_UNSAFE_CHAR_MAP[char] ?? char);
}

function includesWorkflowRoute(surface: NitroBuildSurface): boolean {
Expand Down Expand Up @@ -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)};`,
"",
Expand All @@ -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");
}
Expand All @@ -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,
Expand All @@ -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");
}

Expand Down