diff --git a/src/runtime/components/MDCRenderer.vue b/src/runtime/components/MDCRenderer.vue index f0c79e09..ec457c0f 100644 --- a/src/runtime/components/MDCRenderer.vue +++ b/src/runtime/components/MDCRenderer.vue @@ -8,6 +8,7 @@ import type { MDCElement, MDCNode, MDCRoot, MDCData, MDCRenderOptions } from '@n import htmlTags from '../parser/utils/html-tags-list' import { flatUnwrap, nodeTextContent } from '../utils/node' import { pick } from '../utils' +import { validateProps } from '../parser/utils/props' type CreateElement = typeof h @@ -143,12 +144,29 @@ export default defineComponent({ const meta = { ...data, tags, $route: route, runtimeData, updateRuntimeData } + // Tags that are dangerous when selected as root component from frontmatter + const unsafeRootTags = [...dangerousTags, 'iframe'] + + // Determine root tag, sanitizing when derived from frontmatter + const rootTag = (tag || meta.component?.name || meta.component || 'div') as string + const isRootFromContent = !tag && !!meta.component + + // Sanitize dangerous root tags from frontmatter + const sanitizedTag = isRootFromContent && unsafeRootTags.includes(pascalCase(rootTag).toLowerCase()) + ? 'div' + : rootTag + + // Sanitize root props from frontmatter (block event handlers, srcdoc, formaction, etc.) + const sanitizedProps = isRootFromContent && meta.component?.props + ? validateProps(sanitizedTag, meta.component.props) + : meta.component?.props + // Resolve root component - const component: string | ConcreteComponent = tag !== false ? resolveComponentInstance((tag || meta.component?.name || meta.component || 'div') as string) : undefined + const component: string | ConcreteComponent = tag !== false ? resolveComponentInstance(sanitizedTag) : undefined // Return Vue component return component - ? h(component as any, { ...meta.component?.props, class: ctx.class, ...this.$attrs, key: contentKey }, { default: defaultSlotRenderer }) + ? h(component as any, { ...sanitizedProps, class: ctx.class, ...this.$attrs, key: contentKey }, { default: defaultSlotRenderer }) : defaultSlotRenderer?.() function defaultSlotRenderer() { diff --git a/test/basic.test.ts b/test/basic.test.ts index d921b2a5..45aa1f7f 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -29,4 +29,43 @@ describe('ssr', async () => { const res2 = await $fetch('/api/is-custom-element', { query: { tag: 'div' } }) as any expect(res2).toEqual({ tag: 'div', isCustomElement: false }) }) + + it('blocks dangerous root tags from frontmatter', async () => { + const html = await $fetch('/xss/xss-root-component') + + // Extract the content inside #dangerous-iframe + const iframeSection = html.match(/
(.*?)<\/p>/)?.[1] || '' + expect(iframeSection).not.toContain('