CVE-2023-3224: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: Code Injection in GitHub repository nuxt/nuxt prior to 3.5.3.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 9.8
Published: 2023-06-13T18:15Z
Repository: https://github.com/nuxt/nuxt
Language: TypeScript
Files changed: packages/nuxt/src/app/components/nuxt-root.vue, packages/nuxt/src/app/components/test-component-wrapper.ts, packages/nuxt/src/core/templates.ts, packages/schema/src/config/vite.ts, packages/webpack/package.json, packages/webpack/src/presets/base.ts, pnpm-lock.yaml
Commit date: 2023-04-27 11:31:52 +0100
Commit message: fix(nuxt): restrict access to single renderer outside of test/rootDir (#20543)


Diff: diff --git a/packages/nuxt/src/app/components/nuxt-root.vue b/packages/nuxt/src/app/components/nuxt-root.vue
index 698899bc..b4c71f87 100644
--- a/packages/nuxt/src/app/components/nuxt-root.vue
+++ b/packages/nuxt/src/app/components/nuxt-root.vue
@@ -21,11 +21,11 @@ const IslandRenderer = process.server
 
 const nuxtApp = useNuxtApp()
 const onResolve = nuxtApp.deferHydration()
 
 const url = process.server ? nuxtApp.ssrContext.url : window.location.pathname
-const SingleRenderer = process.dev && process.server && url.startsWith('/__nuxt_component_test__/') && defineAsyncComponent(() => import('#build/test-component-wrapper.mjs')
+const SingleRenderer = process.test && process.dev && process.server && url.startsWith('/__nuxt_component_test__/') && /* #__PURE__ */ defineAsyncComponent(() => import('#build/test-component-wrapper.mjs')
   .then(r => r.default(process.server ? url : window.location.href)))
 
 // Inject default route (outside of pages) as active route
 provide('_route', useRoute())
 
diff --git a/packages/nuxt/src/app/components/test-component-wrapper.ts b/packages/nuxt/src/app/components/test-component-wrapper.ts
index 676b821e..b0430c47 100644
--- a/packages/nuxt/src/app/components/test-component-wrapper.ts
+++ b/packages/nuxt/src/app/components/test-component-wrapper.ts
@@ -1,15 +1,22 @@
 import { parseURL } from 'ufo'
 import { defineComponent, h } from 'vue'
 import { parseQuery } from 'vue-router'
+import { resolve } from 'pathe'
+// @ts-expect-error virtual file
+import { devRootDir } from '#build/nuxt.config.mjs'
 
-export default (url:string) => defineComponent({
+export default (url: string) => defineComponent({
   name: 'NuxtTestComponentWrapper',
 
   async setup (props, { attrs }) {
     const query = parseQuery(parseURL(url).search)
     const urlProps = query.props ? JSON.parse(query.props as string) : {}
+    const path = resolve(query.path as string)
+    if (!path.startsWith(devRootDir)) {
+      throw new Error(`[nuxt] Cannot access path outside of project root directory: \`${path}\`.`)
+    }
     const comp = await import(/* @vite-ignore */ query.path as string).then(r => r.default)
     return () => [
       h('div', 'Component Test Wrapper for ' + query.path),
       h('div', { id: 'nuxt-component-root' }, [
         h(comp, { ...attrs, ...props, ...urlProps })
diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts
index f1d941aa..440c4151 100644
--- a/packages/nuxt/src/core/templates.ts
+++ b/packages/nuxt/src/core/templates.ts
@@ -297,11 +297,12 @@ export const nuxtConfigTemplate = {
   filename: 'nuxt.config.mjs',
   getContents: (ctx: TemplateContext) => {
     return [
       ...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
       `export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
-      `export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`
+      `export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
+      `export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`
     ].join('\n\n')
   }
 }
 
 // TODO: Move to kit
diff --git a/packages/schema/src/config/vite.ts b/packages/schema/src/config/vite.ts
index f4ace563..74ad62df 100644
--- a/packages/schema/src/config/vite.ts
+++ b/packages/schema/src/config/vite.ts
@@ -1,6 +1,7 @@
 import { resolve } from 'pathe'
+import { isTest } from 'std-env'
 import { withoutLeadingSlash } from 'ufo'
 import { defineUntypedSchema } from 'untyped'
 
 export default defineUntypedSchema({
   /**
@@ -19,10 +20,11 @@ export default defineUntypedSchema({
       $resolve: async (val, get) => val ?? (await get('dev') ? 'development' : 'production')
     },
     define: {
       $resolve: async (val, get) => ({
         'process.dev': await get('dev'),
+        'process.test': isTest,
         ...val || {}
       })
     },
     resolve: {
       extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
diff --git a/packages/webpack/package.json b/packages/webpack/package.json
index d4e1d9d7..8aa163dd 100644
--- a/packages/webpack/package.json
+++ b/packages/webpack/package.json
@@ -43,10 +43,11 @@
     "pify": "^6.1.0",
     "postcss": "^8.4.23",
     "postcss-import": "^15.1.0",
     "postcss-loader": "^7.2.4",
     "postcss-url": "^10.1.3",
+    "std-env": "^3.3.2",
     "time-fix-plugin": "^2.0.7",
     "ufo": "^1.1.1",
     "unplugin": "^1.3.1",
     "url-loader": "^4.1.1",
     "vue-bundle-renderer": "^1.0.3",
diff --git a/packages/webpack/src/presets/base.ts b/packages/webpack/src/presets/base.ts
index e1237b92..3cc59e0b 100644
--- a/packages/webpack/src/presets/base.ts
+++ b/packages/webpack/src/presets/base.ts
@@ -8,10 +8,11 @@ import { logger } from '@nuxt/kit'
 // @ts-expect-error missing types
 import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
 import escapeRegExp from 'escape-string-regexp'
 import { joinURL } from 'ufo'
 import type { NuxtOptions } from '@nuxt/schema'
+import { isTest } from 'std-env'
 import type { WarningFilter } from '../plugins/warning-ignore'
 import WarningIgnorePlugin from '../plugins/warning-ignore'
 import type { WebpackConfigContext } from '../utils/config'
 import { applyPresets, fileName } from '../utils/config'
 
@@ -231,10 +232,11 @@ function getEnv (ctx: WebpackConfigContext) {
 
   const _env: Record<string, string | boolean> = {
     'process.env.NODE_ENV': JSON.stringify(ctx.config.mode),
     'process.mode': JSON.stringify(ctx.config.mode),
     'process.dev': options.dev,
+    'process.test': isTest,
     __NUXT_VERSION__: JSON.stringify(ctx.nuxt._version),
     'process.env.VUE_ENV': JSON.stringify(ctx.name),
     'process.browser': ctx.isClient,
     'process.client': ctx.isClient,
     'process.server': ctx.isServer
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2ad71d38..10ac1754 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1042,10 +1042,13 @@ importers:
         specifier: ^7.2.4
         version: 7.2.4(@types/node@18.16.0)(postcss@8.4.23)(ts-node@10.9.1)(typescript@5.0.4)(webpack@5.80.0)
       postcss-url:
         specifier: ^10.1.3
         version: 10.1.3(postcss@8.4.23)
+      std-env:
+        specifier: ^3.3.2
+        version: 3.3.2
       time-fix-plugin:
         specifier: ^2.0.7
         version: 2.0.7(webpack@5.80.0)
       ufo:
         specifier: ^1.1.1

Vulnerable code: const SingleRenderer = process.dev && process.server && url.startsWith('/__nuxt_component_test__/') && defineAsyncComponent(() => import('#build/test-component-wrapper.mjs')
export default (url:string) => defineComponent({
      `export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`
Fixed code: const SingleRenderer = process.test && process.dev && process.server && url.startsWith('/__nuxt_component_test__/') && /* #__PURE__ */ defineAsyncComponent(() => import('#build/test-component-wrapper.mjs')
import { resolve } from 'pathe'
// @ts-expect-error virtual file
import { devRootDir } from '#build/nuxt.config.mjs'
export default (url: string) => defineComponent({
    const path = resolve(query.path as string)
    if (!path.startsWith(devRootDir)) {
      throw new Error(`[nuxt] Cannot access path outside of project root directory: \`${path}\`.`)
    }
      `export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
      `export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`
import { isTest } from 'std-env'
        'process.test': isTest,
    "std-env": "^3.3.2",
import { isTest } from 'std-env'
    'process.test': isTest,
      std-env:
        specifier: ^3.3.2
        version: 3.3.2