Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ee6c846

Browse filesBrowse files
committed
fix(nuxt): reject reserved template island prop under runtime compiler
1 parent 2c981cb commit ee6c846
Copy full SHA for ee6c846

8 files changed

+113-1Lines changed: 113 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎packages/nitro-server/src/index.ts‎

Copy file name to clipboardExpand all lines: packages/nitro-server/src/index.ts
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ export async function bundle (nuxt: Nuxt & { _nitro?: Nitro }): Promise<void> {
782782
const appDir = nuxt.options.alias['#app']
783783
if (appDir) {
784784
tsConfig.compilerOptions.paths['#app/island-hash'] ||= [resolve(appDir, 'island-hash')]
785+
tsConfig.compilerOptions.paths['#app/island-props'] ||= [resolve(appDir, 'island-props')]
785786
tsConfig.compilerOptions.paths['#app/internal/*'] ||= [resolve(appDir, 'internal/*')]
786787
tsConfig.compilerOptions.paths['#app/types'] ||= [resolve(appDir, 'types')]
787788
}
Collapse file

‎packages/nitro-server/src/runtime/diagnostics.ts‎

Copy file name to clipboardExpand all lines: packages/nitro-server/src/runtime/diagnostics.ts
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,10 @@ export const serverDiagnostics = /* #__PURE__ */ defineDiagnostics({
4444
fix: 'Ensure the Nuxt build completed successfully and the server entry was emitted by your builder.',
4545
docs: false,
4646
},
47+
NUXT_E8005: {
48+
why: 'Island props cannot contain a `template` key, which the Vue runtime compiler would compile and execute.',
49+
fix: 'Rename the prop (e.g. `templateName`), or disable `vue.runtimeCompiler` if you do not need runtime template compilation.',
50+
docs: false,
51+
},
4752
},
4853
})
Collapse file

‎packages/nitro-server/src/runtime/handlers/island.ts‎

Copy file name to clipboardExpand all lines: packages/nitro-server/src/runtime/handlers/island.ts
+14-1Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { VueResolver, walkResolver } from '@unhead/vue/utils'
88
import { getRequestDependencies } from 'vue-bundle-renderer/runtime'
99
import { getQuery as getURLQuery } from 'ufo'
1010
import { filterIslandProps, getIslandHash } from '#app/island-hash'
11+
import { findUnsafeIslandPropKey } from '#app/island-props'
1112
import { MAX_ISLAND_BODY_BYTES, exceedsMaxBytes, exceedsMaxDepth } from '../utils/island-props'
1213
import type { NuxtIslandContext, NuxtIslandResponse } from '#app/types'
1314
import { traceAsync } from '#app/internal/tracing'
14-
import { tracingChannelNuxt } from '#internal/nuxt.config.mjs'
15+
import { runtimeCompiler, tracingChannelNuxt } from '#internal/nuxt.config.mjs'
16+
import { serverDiagnostics } from '../diagnostics'
1517
import { islandCache, islandPropCache } from '../utils/cache'
1618
import { createSSRContext } from '../utils/renderer/app'
1719
import { getSSRRenderer } from '../utils/renderer/build-files'
@@ -250,6 +252,17 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
250252
throw createError({ statusCode: 400, statusMessage: 'Invalid island request hash' })
251253
}
252254

255+
// A `template` prop is only executable with the runtime compiler bundled, so this reject
256+
// (which would otherwise trip on legitimate data keyed `template`) is scoped to it.
257+
if (runtimeCompiler && findUnsafeIslandPropKey(parsedProps)) {
258+
// The detail goes to the server console; echoing it would confirm to an unauthenticated
259+
// caller that the runtime compiler is bundled.
260+
if (import.meta.dev) {
261+
serverDiagnostics.NUXT_E8005()
262+
}
263+
throw createError({ statusCode: 400, statusMessage: 'Invalid island request props' })
264+
}
265+
253266
return {
254267
url: typeof rawContext?.url === 'string' ? rawContext.url : '/',
255268
id: hashId,
Collapse file
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export type UnsafeIslandPropKey = 'template'
2+
3+
/**
4+
* Find a `template` key anywhere in island props. With the Vue runtime compiler bundled, a
5+
* `template` string reaching component resolution would be compiled and executed. (`render`
6+
* is not checked: props arrive as JSON, so it can only be an inert string, never a function.)
7+
*
8+
* @internal
9+
*/
10+
export function findUnsafeIslandPropKey (value: unknown): UnsafeIslandPropKey | undefined {
11+
const pending = [value]
12+
const seen = new Set<object>()
13+
14+
while (pending.length) {
15+
const current = pending.pop()
16+
if (!current || typeof current !== 'object' || seen.has(current)) {
17+
continue
18+
}
19+
seen.add(current)
20+
21+
for (const key of Object.keys(current)) {
22+
if (key === 'template') {
23+
return key
24+
}
25+
pending.push((current as Record<string, unknown>)[key])
26+
}
27+
}
28+
}
Collapse file

‎packages/nuxt/src/core/templates.ts‎

Copy file name to clipboardExpand all lines: packages/nuxt/src/core/templates.ts
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
619619
`export const asyncCallHook = ${!!ctx.nuxt.options.experimental.asyncCallHook}`,
620620
`export const clientNodePlaceholder = ${!!ctx.nuxt.options.experimental.clientNodePlaceholder}`,
621621
`export const tracingChannelNuxt = ${!!(ctx.nuxt.options.tracingChannel && typeof ctx.nuxt.options.tracingChannel === 'object' && ctx.nuxt.options.tracingChannel.nuxt)}`,
622+
`export const runtimeCompiler = ${!!ctx.nuxt.options.vue.runtimeCompiler}`,
622623
`export const hasPluginDependencies = ${pluginsHaveDependencies}`,
623624
`export const hasParallelPlugins = ${pluginsRunInParallel}`,
624625
`export const hasPluginHooks = ${pluginsHaveHooks}`,
Collapse file

‎packages/nuxt/test/diagnostics-catalog.test.ts‎

Copy file name to clipboardExpand all lines: packages/nuxt/test/diagnostics-catalog.test.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { renderDiagnostics } from '../src/app/diagnostics/render.ts'
2121
import { manifestDiagnostics } from '../src/app/diagnostics/manifest.ts'
2222
import { unheadDiagnostics } from '../src/app/diagnostics/head.ts'
2323
import { stateDiagnostics } from '../src/app/diagnostics/state.ts'
24+
import { serverDiagnostics } from '../../nitro-server/src/runtime/diagnostics.ts'
2425

2526
const catalogs = [
2627
buildDiagnostics,
@@ -38,6 +39,7 @@ const catalogs = [
3839
manifestDiagnostics,
3940
unheadDiagnostics,
4041
stateDiagnostics,
42+
serverDiagnostics,
4143
]
4244

4345
describe('diagnostics catalog', () => {
Collapse file

‎packages/nuxt/test/island-hash.test.ts‎

Copy file name to clipboardExpand all lines: packages/nuxt/test/island-hash.test.ts
+52Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import { hash } from 'ohash'
33
import { filterIslandProps, getIslandHash, serializeIslandProps } from '#app/island-hash'
4+
import { findUnsafeIslandPropKey } from '#app/island-props'
45

56
describe('filterIslandProps', () => {
67
it('returns an empty object for nullish input', () => {
@@ -125,3 +126,54 @@ describe('getIslandHash', () => {
125126
}
126127
})
127128
})
129+
130+
describe('findUnsafeIslandPropKey', () => {
131+
it('returns undefined for null and undefined', () => {
132+
expect(findUnsafeIslandPropKey(null)).toBeUndefined()
133+
expect(findUnsafeIslandPropKey(undefined)).toBeUndefined()
134+
})
135+
136+
it('returns undefined for primitives', () => {
137+
expect(findUnsafeIslandPropKey(42)).toBeUndefined()
138+
expect(findUnsafeIslandPropKey('hello')).toBeUndefined()
139+
expect(findUnsafeIslandPropKey(true)).toBeUndefined()
140+
})
141+
142+
it('returns undefined for plain objects without a template key', () => {
143+
expect(findUnsafeIslandPropKey({ foo: 1, bar: 'baz' })).toBeUndefined()
144+
expect(findUnsafeIslandPropKey({ content: { title: 'Hello' }, items: [{ label: 'One' }] })).toBeUndefined()
145+
})
146+
147+
it('returns undefined for arrays without a template key', () => {
148+
expect(findUnsafeIslandPropKey([1, 2, 3])).toBeUndefined()
149+
expect(findUnsafeIslandPropKey([{ ok: true }])).toBeUndefined()
150+
})
151+
152+
it('returns the template key at the top level', () => {
153+
expect(findUnsafeIslandPropKey({ template: '<div>evil</div>' })).toBe('template')
154+
})
155+
156+
it('returns the template key when nested in an object', () => {
157+
expect(findUnsafeIslandPropKey({ as: { template: '<div />' } })).toBe('template')
158+
})
159+
160+
it('returns the template key when nested in an array', () => {
161+
expect(findUnsafeIslandPropKey({ items: [{ id: 1 }, { template: 'evil' }] })).toBe('template')
162+
})
163+
164+
it('does not match keys that merely contain template as a substring', () => {
165+
expect(findUnsafeIslandPropKey({ template_id: 42, pretemplate: true })).toBeUndefined()
166+
})
167+
168+
it('only considers own enumerable properties', () => {
169+
const inherited = Object.create({ template: '<div />' })
170+
inherited.label = 'safe'
171+
expect(findUnsafeIslandPropKey({ inherited })).toBeUndefined()
172+
})
173+
174+
it('handles circular values without infinite loop', () => {
175+
const value: Record<string, unknown> = {}
176+
value.self = value
177+
expect(findUnsafeIslandPropKey(value)).toBeUndefined()
178+
})
179+
})
Collapse file

‎test/server-components.test.ts‎

Copy file name to clipboardExpand all lines: test/server-components.test.ts
+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,16 @@ describe('hash/render input alignment', () => {
622622
})
623623
})
624624

625+
describe('reserved island prop keys', () => {
626+
// Without `vue.runtimeCompiler` a `template` value is inert, so data that merely contains
627+
// that key (e.g. CMS content) must still render.
628+
it('allows a nested template key when the runtime compiler is disabled', async () => {
629+
const props = { content: { template: 'blog' } }
630+
const result = await $fetch<NuxtIslandResponse>(islandURL('PureComponent', { props }))
631+
expect(result.html).toBeTruthy()
632+
})
633+
})
634+
625635
describe('page-island middleware', () => {
626636
it('runs page middleware and honours redirects for `page_*` islands', async () => {
627637
const res = await fetch(islandURL('page_gated-server-page', {

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.