Skip to content

Commit 3dcf8c1

Browse files
authored
fix: sanitize component props (#543)
1 parent 284540a commit 3dcf8c1

File tree

3 files changed

+44
-1
lines changed

3 files changed

+44
-1
lines changed

src/runtime/server/og-image/context.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useNitroApp } from 'nitropack/runtime'
1616
import { hash } from 'ohash'
1717
import { parseURL, withoutLeadingSlash, withoutTrailingSlash, withQuery } from 'ufo'
1818
import { normalizeKey } from 'unstorage'
19-
import { decodeOgImageParams, extractEncodedSegment, separateProps } from '../../shared'
19+
import { decodeOgImageParams, extractEncodedSegment, sanitizeProps, separateProps } from '../../shared'
2020
import { autoEjectCommunityTemplate } from '../util/auto-eject'
2121
import { createNitroRouteRuleMatcher } from '../util/kit'
2222
import { normaliseOptions } from '../util/options'
@@ -131,6 +131,10 @@ export async function resolveContext(e: H3Event): Promise<H3Error | OgImageRende
131131
const ogImageRouteRules = separateProps(routeRules.ogImage as RouteRulesOgImage)
132132
const options = defu(queryParams, urlOptions, ogImageRouteRules, runtimeConfig.defaults) as OgImageOptionsInternal
133133

134+
// Strip HTML event handlers and dangerous attributes from props (GHSA-mg36-wvcr-m75h)
135+
if (options.props && typeof options.props === 'object')
136+
options.props = sanitizeProps(options.props as Record<string, any>)
137+
134138
if (!options) {
135139
return createError({
136140
statusCode: 404,

src/runtime/shared.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ export function separateProps(options: OgImageOptions | undefined, ignoreKeys: s
100100
return result as OgImageOptions
101101
}
102102

103+
const DANGEROUS_ATTRS = new Set(['autofocus', 'contenteditable', 'tabindex', 'accesskey'])
104+
105+
/**
106+
* Strip HTML event handlers and dangerous attributes from props to prevent
107+
* reflected XSS via Vue fallthrough attributes (GHSA-mg36-wvcr-m75h).
108+
*/
109+
export function sanitizeProps(props: Record<string, any>): Record<string, any> {
110+
const clean: Record<string, any> = {}
111+
for (const key of Object.keys(props)) {
112+
if (key.startsWith('on') || DANGEROUS_ATTRS.has(key.toLowerCase()))
113+
continue
114+
clean[key] = props[key]
115+
}
116+
return clean
117+
}
118+
103119
export function withoutQuery(path: string) {
104120
return path.split('?')[0]
105121
}

test/unit/security.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { sanitizeProps } from '../../src/runtime/shared'
3+
4+
describe('sanitizeProps (GHSA-mg36-wvcr-m75h)', () => {
5+
it('strips on* event handlers', () => {
6+
const result = sanitizeProps({ title: 'Hello', onmouseover: 'alert(1)', onclick: 'steal()' })
7+
expect(result).toEqual({ title: 'Hello' })
8+
})
9+
10+
it('strips dangerous HTML attributes', () => {
11+
const result = sanitizeProps({ title: 'Hi', autofocus: '', contenteditable: 'true', tabindex: '1', accesskey: 'x' })
12+
expect(result).toEqual({ title: 'Hi' })
13+
})
14+
15+
it('preserves legitimate props', () => {
16+
const result = sanitizeProps({ title: 'Test', description: 'A page', colorMode: 'dark', theme: '#fff' })
17+
expect(result).toEqual({ title: 'Test', description: 'A page', colorMode: 'dark', theme: '#fff' })
18+
})
19+
20+
it('handles empty props', () => {
21+
expect(sanitizeProps({})).toEqual({})
22+
})
23+
})

0 commit comments

Comments
 (0)