Skip to content

fix: whitelist component props to prevent cache key DoS#544

Merged
harlan-zw merged 5 commits intomainfrom
fix/prop-whitelist-dos-prevention
Mar 26, 2026
Merged

fix: whitelist component props to prevent cache key DoS#544
harlan-zw merged 5 commits intomainfrom
fix/prop-whitelist-dos-prevention

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

@harlan-zw harlan-zw commented Mar 26, 2026

🔗 Linked issue

N/A (proactive security hardening)

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking 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:

  1. Extracts prop names at build time using @vue/compiler-sfc's compileScript to get the definitive list of declared props from each component's defineProps. Adds propNames: string[] to OgImageComponent.

  2. Filters unknown props at runtime in resolveContext. Any props not in the component's whitelist are silently dropped (with a dev-mode warning). Components without defineProps accept no props.

  3. 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 where cacheQueryParams allowed arbitrary params to inflate cache storage.

The propNames array is available on componentNames in the debug.json devtools endpoint.

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.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 26, 2026

Open in StackBlitz

npm i https://pkg.pr.new/nuxt-og-image@544

commit: 818ba57

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 defineProps prop-name extraction via @vue/compiler-sfc (extractPropNamesFromVue) plus unit tests.
  • Extend OgImageComponent metadata with optional propNames and populate it during components:extend.
  • Filter/drops runtime options.props keys 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.

Comment on lines +149 to +164
// 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
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 818ba57resolvePathCacheKey now hashes the resolved options object (post-filtering) instead of getQuery(e). Arbitrary query params no longer produce unique cache keys.

Comment on lines 1015 to 1027
@@ -1017,6 +1023,7 @@ export default defineNuxtModule<ModuleOptions>({
category,
credits,
renderer,
propNames,
})
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@harlan-zw harlan-zw merged commit bd05a77 into main Mar 26, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants