fix: whitelist component props to prevent cache key DoS#544
Conversation
Extract prop names from defineProps at build time and filter unknown props at runtime. Prevents attackers from inflating cache entries by varying arbitrary query params.
commit: |
Replace regex-based prop parsing with the SFC compiler's compileScript API which reliably resolves bindings for all defineProps syntaxes. Pre-loads the compiler before the synchronous components:extend hook.
There was a problem hiding this comment.
Pull request overview
This PR aims to mitigate cache-key inflation/DoS risk by introducing build-time extraction of Vue SFC defineProps names and enforcing a runtime props whitelist for OG image rendering.
Changes:
- Add build-time
definePropsprop-name extraction via@vue/compiler-sfc(extractPropNamesFromVue) plus unit tests. - Extend
OgImageComponentmetadata with optionalpropNamesand populate it duringcomponents:extend. - Filter/drops runtime
options.propskeys that aren’t declared by the selected component (dev warning on drop).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit/props.test.ts | Adds unit coverage for defineProps prop-name extraction across supported syntaxes. |
| src/runtime/types.ts | Extends OgImageComponent to carry build-extracted propNames. |
| src/runtime/server/og-image/context.ts | Applies runtime props whitelist (drops undeclared props; warns in dev). |
| src/module.ts | Preloads @vue/compiler-sfc and attaches extracted propNames to component metadata. |
| src/build/props.ts | Implements compiler-backed prop-name extraction helper + lazy compiler loading. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Whitelist props: only allow props declared in the component's defineProps. | ||
| // Components without defineProps accept no props. Prevents cache key inflation | ||
| // from arbitrary query params (DoS vector). | ||
| if (normalised.component && normalised.options.props && typeof normalised.options.props === 'object') { | ||
| const allowedProps = normalised.component.propNames || [] | ||
| const allowedSet = new Set(allowedProps) | ||
| const raw = normalised.options.props as Record<string, any> | ||
| const filtered: Record<string, any> = {} | ||
| for (const key of Object.keys(raw)) { | ||
| if (allowedSet.has(key)) | ||
| filtered[key] = raw[key] | ||
| else if (import.meta.dev) | ||
| logger.warn(`[Nuxt OG Image] Prop "${key}" is not declared by component "${normalised.component.pascalName}" and was dropped. Declared props: ${allowedProps.join(', ')}`) | ||
| } | ||
| normalised.options.props = filtered | ||
| } |
There was a problem hiding this comment.
The new prop-whitelisting only filters normalised.options.props, but when cacheQueryParams is enabled the cache key still hashes getQuery(e) (see resolvePathCacheKey), which includes all raw query params — including the ones you just dropped. That means an attacker can still inflate cache keys/storage by varying arbitrary query params even though they don’t affect rendering. Consider basing the query hash on a sanitized/whitelisted subset (e.g., only internal OG params + filtered props), or compute the cache key from the filtered normalised.options instead of the raw request query.
There was a problem hiding this comment.
Fixed in 818ba57 — resolvePathCacheKey now hashes the resolved options object (post-filtering) instead of getQuery(e). Arbitrary query params no longer produce unique cache keys.
| @@ -1017,6 +1023,7 @@ export default defineNuxtModule<ModuleOptions>({ | |||
| category, | |||
| credits, | |||
| renderer, | |||
| propNames, | |||
| }) | |||
There was a problem hiding this comment.
PR description mentions exposing propNames via the debug.json devtools endpoint, but in the current runtime handler the JSON response only includes { extract, siteUrl, ...rendererDebug } and none of those paths include component metadata/propNames. Either add propNames (or full component metadata) to the debug payload, or update the PR description to match the implemented behavior.
There was a problem hiding this comment.
Updated the PR description. propNames is available on componentNames in the global debug.json endpoint, not in the per-request devtools context.
resolvePathCacheKey now hashes the filtered/sanitized options object instead of the raw getQuery(e). This closes the gap where cacheQueryParams allowed arbitrary query params to inflate cache keys even after prop filtering.
🔗 Linked issue
N/A (proactive security hardening)
❓ Type of change
📚 Description
OG image endpoints accept arbitrary query params as component props, which means an attacker could generate thousands of unique cache entries by varying prop values (e.g.
?title=aaaa,?title=aaab). Each unique combo triggers a real render, exhausting CPU via the semaphore queue and filling cache storage.This PR:
Extracts prop names at build time using
@vue/compiler-sfc'scompileScriptto get the definitive list of declared props from each component'sdefineProps. AddspropNames: string[]toOgImageComponent.Filters unknown props at runtime in
resolveContext. Any props not in the component's whitelist are silently dropped (with a dev-mode warning). Components withoutdefinePropsaccept no props.Derives cache keys from resolved options instead of raw
getQuery(e). This ensures the prop whitelist and sanitization are reflected in the cache key, closing the gap wherecacheQueryParamsallowed arbitrary params to inflate cache storage.The
propNamesarray is available oncomponentNamesin thedebug.jsondevtools endpoint.