vue-ssr-security-review
Statically review Vue 3 SSR entry points and templates for cross-request state pollution (module-scope reactive state, non-per-request app/store creation) and injection via unsanitized v-html or unvalidated dynamic href/src bindings, grounded in Vue's own SSR and security-best-pr
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/vue-ssr-security-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Vue SSR Security Review
Purpose
Review Vue 3 server-side-rendered entry points, module-scope state, and template bindings for the two SSR-specific defect classes Vue's own documentation calls out directly: cross-request state pollution from state that is not created fresh per request, and injection through unsanitized v-html or unvalidated dynamic :href/:src bindings — without re-litigating composable/reactivity architecture, hydration-mismatch mechanics, or general component design in every response. This skill exists so the review stays anchored to the two documented, security-critical defect classes instead of drifting into a general "SSR code review."
When to use
Use this skill when the user asks to:
- review an SSR entry point (
entry-server.js/.ts, or equivalent request-handling code that creates the Vue app for rendering), - assess whether a
v-htmlusage is safe, - investigate a report of users seeing another user's data on first load or on a subsequent request — the classic cross-request state pollution symptom,
- perform a pre-launch security review of an SSR Vue application.
Do not use this skill for:
- a purely client-rendered (non-SSR) Vue app with no server-rendering entry point — cross-request state pollution does not apply, because each browser tab has its own isolated JS realm and there is no shared server process handling concurrent requests,
- Options API/Composition API architecture review with no security angle (composable extraction quality, reactivity-boundary correctness) — use
vue-composition-api-architecture-reviewinstead, - a bug that requires live traffic reproduction (concurrent-request load testing, session-replay capture) to confirm exploitation — static analysis proves the structural risk, not that it has already been exploited in production.
Context7 Documentation Protocol
- Resolve the Vue library ID with
resolve-library-id(matched result:/vuejs/vue) before citing any SSR-mechanism orv-html-behavior claim. /vuejs/vueis Vue's core source-and-test repository (its SSR test fixtures andserver-rendererpackage source cover Vue 2'svue-server-renderer), not the Vue 3 prose docs site. Usequery-docsagainst it only to corroborate low-level rendering mechanics (e.g., thatv-htmlcompiles directly to setting theinnerHTMLDOM property with no sanitization step). It does not reliably surface the Vue 3 SSR guide's per-request-instance narrative or the Security guide'sv-html/dynamic-binding rules — for that guidance, use theofficial_docsURLs in this skill'smetadata.jsonand label the claimdocumentation-based.- Before flagging a specific pattern as cross-request state pollution, confirm the app is actually SSR (a request-handling entry point exists that renders on the server) — the pollution risk is specific to SSR's single, long-lived Node.js process handling many requests; it does not apply to a client-only SPA.
- Read
package.jsonfirst to confirm which Vue major and SSR toolchain are in use (vue-server-rendererfor Vue 2,@vue/server-renderer/ a meta-framework like Nuxt for Vue 3) — the per-request app/store creation requirement and the exact API names differ by major version and toolchain; do not apply Vue 3 API names to a Vue 2 codebase or vice versa. - If Context7 is unavailable, fall back to the
official_docsURLs in this skill'smetadata.jsonand label the claimdocumentation-based, unverified against current release.
Lean operating rules
- Cross-request state pollution and injection findings default to HIGH severity. This is a security-scoped skill: do not downgrade a structural cross-request-pollution risk or an untraced
v-htmlsanitizer gap to MEDIUM just because it has not been observed exploited yet — the risk is in the structure, not in whether someone has already hit it. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this might leak state" or "this v-html might be unsafe" without showing the specific module-scope declaration, the specific reachability path, or the specific unsanitized data-flow trace is not a valid finding — it is a guess.
- Do not treat every module-scope declaration as a pollution risk. An immutable, non-reactive constant (a route table, a static config object, a compiled template) declared at module scope is safe. Only mutable or reactive state reachable from an SSR-rendered component's render path is the risk — check both properties (mutability/reactivity, and reachability) before flagging.
- Do not approve a
v-htmlbinding whose data source includes any user-reachable input (route params, query strings, request bodies, third-party API responses that themselves echo user input) unless a named sanitizer call (e.g., DOMPurify) is visibly present on that exact data-flow path. A sanitizer import existing elsewhere in the codebase does not clear this bar — trace the specific path under review. - Check dynamic
:href/:srcbindings for scheme validation (an allowlist rejectingjavascript:and other non-http(s)schemes) whenever the bound value's source includes user-reachable input. An unvalidated dynamic URL binding fed by user input is a MEDIUM-to-HIGH finding depending on reachability from an authenticated or public surface. - Watch for per-request factory functions that appear correct (a fresh
createApp()/createSSRApp()call per request) but still close over a shared module-level cache, singleton, or default parameter passed in from outer scope — the factory pattern alone does not guarantee isolation if it references mutable shared state from its enclosing scope. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only).
- Load only the reference needed for the concern in scope.
References
Load these only when needed:
- Review workflow and findings contract — use for the step-by-step review procedure, the state-pollution/injection decision tree, and the required output shape.
- Cross-request state pollution — load only when reviewing an SSR entry point, tracing app/store/router creation, or investigating a suspected cross-request data-leak symptom.
- Injection: v-html and dynamic URL bindings — load only when the review scope includes a
v-htmlusage or a dynamic:href/:srcbinding. Includes the OWASP XSS grounding reference; load that citation only when av-htmlfinding is actually present.
Response minimum
Return, at minimum:
- the SSR entry point(s), module-scope declarations, and/or template bindings in scope,
- ranked findings with file:line evidence, defect category (
state-pollution,xss, orurl-injection), the concrete data-flow trace (module-scope declaration and its reachability, or the origin-to-sink path for the injection), and a fix sketch matching Vue's documented pattern, - for every
v-htmlfinding, an explicit statement of whether a sanitizer call is present on the traced path — never approve on the assumption one exists elsewhere, - evidence level per finding (
repo evidence,documentation-based, orinference), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block),
- open questions or scope the review could not cover (e.g., "confirming actual cross-request leakage requires concurrent-request load testing, not static review").
Files (vanguard-frontier-agentic)
-
references
-
cross-request-state-pollution.md 6.9 KB
# Cross-Request State Pollution Use this reference only when reviewing an SSR entry point, tracing app/store/router creation, or investigating a suspected cross-request data-leak symptom (a user reporting they saw another user's data on load). ## What people get wrong The naive assumption is: > "The server renders each request separately, so state can't leak between users." Wrong. A Node.js SSR server is a single long-lived process handling many concurrent requests on shared module-level memory. If the Vue app instance, its store, or any reactive state it reads is created once at module load time — rather than freshly for each request — every request after the first reads and mutates the *same* in-memory objects. One user's session data, cart contents, or auth state can render into another user's response. This is not a rare edge case; it is the default failure mode of naively porting client-only Vue code to an SSR entry point. ## Officially grounded requirement Vue's own server-side-rendering guidance is explicit and non-negotiable on this point: **each incoming request must get a fresh, isolated instance of the root Vue app** (and, by extension, any store or router it depends on) rather than sharing one instance across requests. This is the single most important structural rule for SSR safety and is documented as a requirement, not a recommendation (`documentation-based`, Vue SSR guide). The practical shape of the rule: - The SSR entry point exports (or defines inline) a per-request factory function. - That factory function is invoked once per incoming request. - Inside that factory, `createSSRApp()` (or the toolchain-equivalent app-creation call) runs fresh — along with any Pinia/Vuex store instance and router instance the app needs — and only *that* freshly created instance is used to render the current request's response. - Nothing about the freshly created instance is retained, cached, or reused for a subsequent request. ## Non-negotiable design rules ### 1. Trace app creation to its actual call site, not its apparent location Do not accept "there's a `createSSRApp()` call in this file" as sufficient. Confirm it executes *inside* the function that runs per request, not at module top level where it would execute once when the server process starts. ### 2. Store and router instances follow the same rule as the app instance A correctly per-request `createSSRApp()` call does not fix a module-scope Pinia store or a module-scope router instance created once and imported into every request's app tree. Check store/router creation with the same rigor as the app instance itself — these are frequently overlooked because the app-creation call "looks right" while the store creation nearby does not. ### 3. Closures over shared mutable state defeat an otherwise-correct factory A per-request factory function that itself creates a fresh app can still leak if it closes over a module-level cache, singleton, or a mutable default parameter supplied from outer scope. Read the full body of the factory function, not just its `createSSRApp()` line — check every variable it references from enclosing scope, and classify each as immutable/safe or mutable/reactive/shared. ### 4. Immutable constants are not the risk; mutable and reactive state is A module-scope `const ROUTES = [...]` frozen route table, a static config object with no runtime mutation path, or a compiled template string is safe to share across requests — it never changes after module load. The risk is specifically state that is either declared with `ref()`/`reactive()` (Vue's reactivity primitives, which are designed to be mutated and observed) or is a plain mutable object/array that request-handling code writes to. Classify every module-scope declaration on this axis before flagging it. ### 5. Reachability from the SSR-rendered tree is required for a finding A mutable module-scope object that no SSR-rendered component or composable ever imports, reads, or writes is not a reachable risk in this review's scope (it may still be dead code or a different kind of bug, but it is not a cross-request pollution finding). Trace the import graph from the flagged declaration to at least one component or composable actually rendered during SSR before calling it a finding. ## Minimal safe implementation pattern The safe shape, matching Vue's documented guidance: ```js // entry-server.js export async function render(url, context) { // Fresh, per-request instances — nothing here is created at module scope. const { app, router, store } = createApp() router.push(url) await router.isReady() // Populate store state for this request only; store was just created above. const html = await renderToString(app, context) // app, router, store all go out of scope when this function returns. return html } ``` Anti-pattern (module-scope creation — do not approve): ```js // entry-server.js — WRONG: created once at module load, shared across all requests const app = createApp() const store = createStore() export async function render(url, context) { router.push(url) await router.isReady() return renderToString(app, context) // every request renders the SAME app/store instance } ``` ## Adversarial checklist Before clearing an SSR entry point as safe from cross-request pollution, answer these: - Does the app-creation call execute inside the per-request handler, or at module scope? - Does the store (if any) get created fresh inside that same per-request scope, or is it a module-level singleton imported into the app? - Does the router get created fresh inside that same per-request scope? - Does the per-request factory function close over any variable from its enclosing module scope that is mutable or reactive? - Is there any module-scope cache (e.g., a component-render cache, a computed-property memoization keyed loosely, a "last user" convenience variable) that a developer might have added for performance and forgotten carries cross-request state? - If two requests from different users hit this entry point concurrently, is there any shared object either request's handling code could write to that the other request's handling code could read? If any answer reveals a "yes" to shared mutable reachable state, or the app/store/router creation cannot be confirmed as per-request, the finding is HIGH and structural — report it even without a reproduced incident. ## Verification targets - Grep the SSR entry file for `createApp(` / `createSSRApp(` / `createStore(` / `createRouter(` and confirm each call site's enclosing scope (module-level `import`/top-level statement vs. inside an exported/invoked function). - Grep for `let `/`var `/mutable `const` object or array literals at module scope in files imported by the SSR entry point or any SSR-rendered component. - Grep for `ref(`/`reactive(` calls outside of a component's `setup()` or a composable function body — a `ref()`/`reactive()` call at true module scope (not inside any function) is the clearest structural signal of this defect class. -
injection-and-dynamic-urls.md 8.7 KB
# Injection: v-html and Dynamic URL Bindings Use this reference only when the review scope includes a `v-html` usage or a dynamic `:href`/`:src` binding. The OWASP XSS citation below is loaded only when a `v-html` finding is actually present in the review — do not cite it preemptively in a review that has no `v-html` usage. ## What people get wrong The naive assumption is: > "Vue escapes everything by default, so if I'm using `v-html` I must already know it's a special case and I've handled it." Partially wrong. Vue's default text interpolation (`{{ }}`) does auto-escape, and that is precisely why `v-html` exists as an explicit escape hatch — but "I chose `v-html` on purpose" is not the same as "I sanitized the content that flows into it." The recurring real-world failure is not developers being unaware `v-html` is dangerous; it is developers sanitizing at one layer (e.g., a markdown renderer) while a *different* unsanitized value reaches the same `v-html` binding through a later code change, or trusting a third-party API response as "safe" because it isn't literally user-typed input even though it echoes user-submitted content back. ## Officially grounded rules Vue's own security best-practices guidance states directly: - **`v-html` on untrusted content is unsafe.** Dynamically rendering arbitrary HTML on your site via `v-html` can be dangerous because it can easily lead to XSS vulnerabilities. Only use `v-html` on content you can trust to be safe, or content sanitized by a dedicated library before it reaches the binding (`documentation-based`, Vue security guide). - **Dynamic attribute/URL bindings need scheme validation.** Vue's docs specifically call out `:href`/`:src`-style dynamic bindings as an injection surface distinct from `v-html`: unvalidated user-provided URLs bound dynamically can carry a `javascript:` (or similarly dangerous non-`http(s)`) scheme, executing script when the element is interacted with, even though no HTML markup or `v-html` was involved. - **Template injection is a separate, related risk** for any code path that compiles user-supplied strings as Vue templates at runtime — out of scope for a typical SSR entry/template review unless the app does this explicitly (e.g., a CMS that lets users author raw Vue template syntax); flag its presence if found, but it is not the default pattern to hunt for. The low-level mechanism, confirmed by Vue's own compiler source (`repo evidence` via Context7 `/vuejs/vue`): `v-html` compiles directly to setting the element's `innerHTML` DOM property with the bound value stringified — there is no implicit sanitization step anywhere in that compilation path. Sanitization, if it happens at all, must be applied by application code before the value reaches the binding. ## Non-negotiable design rules ### 1. Trace the full origin-to-sink path before judging a v-html binding Do not evaluate `v-html="someVar"` in isolation. Follow `someVar` backward: is it a literal string in the template file? A prop? A computed value derived from store state? Store state populated from an API response? An API response that itself echoes a value the current user (or any user) submitted at some point? The finding depends on where that trace terminates, not on the binding syntax alone. ### 2. A sanitizer import elsewhere in the codebase does not clear a specific finding If the trace reveals user-reachable input reaching a `v-html` binding, the only thing that clears the finding is a *named sanitizer call* (e.g., `DOMPurify.sanitize(...)`) visibly present *on that exact path* — between the untrusted origin and the binding. "This codebase has a `sanitizeHtml` utility used elsewhere" is not evidence the specific path under review calls it. ### 3. Third-party API responses are not automatically trusted An API response is not "safe by default" just because it did not come directly from the current request's form input. If the API itself stores and echoes content that any user (not necessarily the current one) previously submitted — a comment system, a CMS with contributor accounts, a product-review feed — that response is user-reachable input for this review's purposes and needs the same sanitizer-on-path check. ### 4. Dynamic URL bindings need scheme validation, not HTML sanitization `:href`/`:src` bindings are a distinct injection surface from `v-html` — do not conflate the two fixes. The correct control for a dynamic URL binding fed by user-reachable input is scheme validation (an allowlist accepting only `http:`/`https:`/`mailto:` as appropriate, rejecting `javascript:` and other schemes), not HTML sanitization. A `v-html`-appropriate sanitizer call does not clear a URL-injection finding and vice versa. ### 5. Origin-controlled content is not automatically a false positive to skip silently If a `v-html` trace terminates at fully origin-controlled content (static marketing copy authored only through the app's own trusted CMS with no user-submission path anywhere upstream), it is correctly not a finding — but state this explicitly in the output rather than omitting the binding from the review entirely. A future code change could introduce a user-reachable path into the same binding, and an explicit "reviewed, not a finding, because X" record is more useful than silence. ## Minimal safe implementation pattern ```vue <script setup> import DOMPurify from 'dompurify' import { computed } from 'vue' const props = defineProps<{ rawComment: string }>() // Sanitizer call sits directly on the path between the untrusted prop and the binding. const safeComment = computed(() => DOMPurify.sanitize(props.rawComment)) </script> <template> <div v-html="safeComment"></div> </template> ``` Anti-pattern (untraced or missing sanitizer — do not approve): ```vue <template> <!-- userBio comes from a profile API that echoes user-submitted text. No sanitizer call anywhere between the API response and this binding. --> <div v-html="userBio"></div> </template> ``` Dynamic URL scheme validation: ```vue <script setup> const ALLOWED_SCHEMES = ['http:', 'https:', 'mailto:'] function safeHref(url) { try { const parsed = new URL(url, window.location.origin) return ALLOWED_SCHEMES.includes(parsed.protocol) ? url : '#' } catch { return '#' } } </script> <template> <a :href="safeHref(userProvidedUrl)">Link</a> </template> ``` ## Adversarial checklist Before clearing a `v-html` binding, answer these: - What is the literal origin of the bound value — a template literal, a prop, computed state, store state, or an API response? - Does any point along that trace involve content any user (current or otherwise) previously submitted? - Is there a named sanitizer call visible on the exact path traced, or only "a sanitizer exists somewhere in this codebase"? - Could a later code change (a refactor that swaps the data source, or a new field added to an existing API response) reach this same binding without re-triggering a security review? Before clearing a dynamic `:href`/`:src` binding, answer these: - Does the bound value's origin include user-reachable input? - Is there scheme validation (allowlist) on the path, or only implicit trust that "URLs are just links"? - Would a crafted `javascript:` (or `data:`, `vbscript:`) value reach this binding unmodified? If any answer is unclear or reveals a gap, the finding is HIGH (for `v-html`) or MEDIUM-to-HIGH (for URL bindings, per reachability) — do not soften it to "worth double-checking." ## OWASP grounding (load only when a v-html finding is present) Cross-Site Scripting (XSS) is the general vulnerability class that unsanitized `v-html` produces: an attacker-controlled or attacker-influenced string is rendered as live HTML/script in another user's browser session. The OWASP XSS reference and OWASP Top Ten (both listed in this skill's `official_docs`) provide the vendor-neutral grounding for why this defect class is treated as HIGH severity by default — it typically enables session/token theft, credential harvesting via injected forms, or full account takeover in the victim's authenticated context, not merely a cosmetic rendering issue. Cite these only in the specific finding write-up for a confirmed or suspected `v-html`/XSS defect, not as boilerplate in every review. ## Verification targets - Grep the template scope (`.vue` files, render functions) for `v-html` and enumerate every match. - Grep for `:href=` and `:src=` bindings (and their shorthand `v-bind:href=`/`v-bind:src=`) bound to a non-literal expression. - For each match, grep backward through the component's props, computed properties, and any imported store/composable for the value's origin. - Grep for a sanitizer import (`dompurify`, or an equivalent project-specific sanitizer utility) and confirm its call site is on the traced path, not merely present in the file or module. -
workflow-and-output.md 6.6 KB
# Review Workflow and Findings Contract Use this reference for the step-by-step review procedure and the required output shape. Load the other two references only for the specific defect class the SSR code or template under review actually raises. ## Prerequisites - Confirm the app is actually SSR: a request-handling entry point exists (`entry-server.js`/`.ts`, a Nuxt/framework server route, or equivalent) that renders Vue output on the server per incoming request. If no such entry point exists, cross-request state pollution is out of scope — flag this explicitly and move directly to the injection-only concerns (`v-html`, dynamic URL bindings), which apply to any Vue app regardless of rendering mode. - Identify the Vue major and SSR toolchain in use (`package.json` — `vue`, `vue-server-renderer` for Vue 2, `@vue/server-renderer` or a meta-framework for Vue 3). Per-request creation APIs and exact guidance differ by major. ## Workflow 1. **Locate every SSR entry point.** For each, read the full request-handling function from the top. 2. **Trace app/store/router creation.** For each entry point, determine whether `createSSRApp()` (or the toolchain's equivalent app-factory call) — and any store (Pinia/Vuex) or router instance the app depends on — is created *inside* the per-request handler function, freshly, for every request. See `references/cross-request-state-pollution.md` for the decision tree. 3. **Enumerate module-scope declarations reachable from SSR-rendered components.** Grep for `ref()`, `reactive()`, plain mutable object/array literals, and module-level caches or singletons declared outside any per-request factory function. For each, check reachability: is it imported, read, or written by any component or composable in the SSR-rendered component tree? 4. **Enumerate every `v-html` binding in scope.** For each, trace its data source backward through props, computed values, store state, and API responses to the origin. Determine whether the origin includes user-reachable input (route params, query strings, request bodies, or a third-party API response that itself echoes user input) and whether a named sanitizer call sits on that exact path. See `references/injection-and-dynamic-urls.md`. 5. **Enumerate every dynamic `:href`/`:src` binding in scope.** For each, trace its data source the same way. Check for scheme validation (an allowlist rejecting `javascript:` and other non-`http(s)` schemes) when the source includes user-reachable input. 6. **Produce ranked findings** using the output contract below. ## Decision tree - SSR entry point creates the app/store/router at module scope (outside the per-request handler) → **HIGH** finding, cross-request state pollution risk. Cite the Vue SSR guide's per-request-instance requirement directly (`documentation-based`). - SSR entry point's per-request factory function closes over a shared module-level mutable cache, singleton, or default parameter from outer scope → **HIGH** finding — the factory pattern alone does not guarantee isolation; name the specific closed-over reference. - A mutable or reactive module-scope declaration (not an immutable constant) is reachable — read or written — from any SSR-rendered component's render path → **HIGH** finding regardless of whether pollution has been observed yet; the risk is structural, not conditional on an incident report. - Module-scope declaration is an immutable, non-reactive constant (static config, compiled route table, frozen lookup object) with no runtime mutation path → not a finding. - `v-html` binding's traced data source includes user-reachable input and no sanitizer call is present on that exact path → **HIGH** finding, XSS. Do not accept "a sanitizer exists elsewhere in this codebase" as clearing this — the trace must show the sanitizer on the specific path reviewed. - `v-html` binding's traced data source is fully origin-controlled with no user-reachable input anywhere in the trace (e.g., static marketing copy authored by the app's own CMS with no user-submission path) → not a finding, but state this explicitly in the output rather than silently omitting it. - Dynamic `:href`/`:src` binding's traced source includes user-reachable input with no scheme allowlist/validation → **MEDIUM-to-HIGH** finding depending on reachability (public unauthenticated surface vs. requiring an authenticated session to trigger). - Dynamic `:href`/`:src` binding's source is either fully origin-controlled or already passes through scheme validation → not a finding. ## Output contract Every response from this skill must return: 1. **Scope** — the SSR entry point(s), module-scope declarations, and/or template bindings reviewed. 2. **Ranked findings** — each with file:line, defect category (`state-pollution` / `xss` / `url-injection`), the concrete data-flow trace (the module-scope declaration and its reachability path, or the full origin-to-sink path for the injection finding, naming every hop), and a fix sketch matching Vue's documented pattern. 3. **Sanitizer status per `v-html` finding** — an explicit statement of whether a sanitizer call is present on the traced path; never infer one exists. 4. **Evidence level per finding** — `repo evidence`, `documentation-based`, or `inference`. Label structural risk findings as structural risk explicitly — do not imply confirmed exploitation without live evidence (e.g., a captured cross-user response, a load-test reproduction). 5. **Verdict** — approve / approve-with-notes / block. 6. **Open questions or out-of-scope items** — e.g., "confirming actual cross-request leakage requires concurrent-request load testing, not static review," or "hydration-mismatch risk in this same file is out of scope — recommend a hydration-focused review if the framework is Angular-equivalent, otherwise out of scope for this Vue-focused skill." ## When to push back Push back if the user asks to: - approve a `v-html` usage because "we sanitize elsewhere in the app" without a sanitizer call visible on the specific traced path — that is not evidence, it is an assumption, - treat a per-request `createSSRApp()` call as sufficient in isolation without checking what it closes over — the factory call passing a surface-level check is not the same as proving isolation, - skip the cross-request-pollution check because "we haven't seen it happen in production" — this defect class is structural and often invisible until concurrent load or a specific request-timing race exposes it; absence of a reported incident is not evidence of absence of the risk, - downgrade an untraced `v-html` finding to informational because "it's probably fine" — this skill's default is HIGH for exactly this class of unproven claim.
-
-
metadata.json 1.4 KB
{ "id": "vue-ssr-security-review", "name": "Vue SSR Security Review", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Reviews Vue 3 SSR entry points and templates for cross-request state pollution (module-scope reactive state, non-per-request app/store/router creation) and injection via unsanitized v-html or unvalidated dynamic href/src bindings, grounding claims via Context7 and Vue's own SSR and security best-practices documentation.", "source_type": "original", "official_docs": [ "https://vuejs.org/guide/scaling-up/ssr.html", "https://vuejs.org/guide/best-practices/security.html", "https://owasp.org/www-project-top-ten/", "https://owasp.org/www-community/attacks/xss/" ], "security_notes": "This skill's entire scope is security-critical: cross-request state pollution is a data-exposure defect (potential cross-tenant/cross-user leakage) and unsanitized v-html is a stored/reflected XSS vector. Every finding in this skill defaults to HIGH severity unless proven otherwise with concrete sanitizer evidence. Static-review-only skill: it reads and greps SSR entry points and template source but never executes, builds, or runs application code, and never sends live requests.", "last_verified": "2026-07-02", "path": "skills/frontend/vue-ssr-security-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 8 KB
--- name: vue-ssr-security-review description: Statically review Vue 3 SSR entry points and templates for cross-request state pollution (module-scope reactive state, non-per-request app/store creation) and injection via unsanitized v-html or unvalidated dynamic href/src bindings, grounded in Vue's own SSR and security-best-practices guidance. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-02" category: security --- # Vue SSR Security Review ## Purpose Review Vue 3 server-side-rendered entry points, module-scope state, and template bindings for the two SSR-specific defect classes Vue's own documentation calls out directly: cross-request state pollution from state that is not created fresh per request, and injection through unsanitized `v-html` or unvalidated dynamic `:href`/`:src` bindings — without re-litigating composable/reactivity architecture, hydration-mismatch mechanics, or general component design in every response. This skill exists so the review stays anchored to the two documented, security-critical defect classes instead of drifting into a general "SSR code review." ## When to use Use this skill when the user asks to: - review an SSR entry point (`entry-server.js`/`.ts`, or equivalent request-handling code that creates the Vue app for rendering), - assess whether a `v-html` usage is safe, - investigate a report of users seeing another user's data on first load or on a subsequent request — the classic cross-request state pollution symptom, - perform a pre-launch security review of an SSR Vue application. Do not use this skill for: - a purely client-rendered (non-SSR) Vue app with no server-rendering entry point — cross-request state pollution does not apply, because each browser tab has its own isolated JS realm and there is no shared server process handling concurrent requests, - Options API/Composition API architecture review with no security angle (composable extraction quality, reactivity-boundary correctness) — use `vue-composition-api-architecture-review` instead, - a bug that requires live traffic reproduction (concurrent-request load testing, session-replay capture) to confirm exploitation — static analysis proves the structural risk, not that it has already been exploited in production. ## Context7 Documentation Protocol - Resolve the Vue library ID with `resolve-library-id` (matched result: `/vuejs/vue`) before citing any SSR-mechanism or `v-html`-behavior claim. - `/vuejs/vue` is Vue's core source-and-test repository (its SSR test fixtures and `server-renderer` package source cover Vue 2's `vue-server-renderer`), not the Vue 3 prose docs site. Use `query-docs` against it only to corroborate low-level rendering mechanics (e.g., that `v-html` compiles directly to setting the `innerHTML` DOM property with no sanitization step). It does not reliably surface the Vue 3 SSR guide's per-request-instance narrative or the Security guide's `v-html`/dynamic-binding rules — for that guidance, use the `official_docs` URLs in this skill's `metadata.json` and label the claim `documentation-based`. - Before flagging a specific pattern as cross-request state pollution, confirm the app is actually SSR (a request-handling entry point exists that renders on the server) — the pollution risk is specific to SSR's single, long-lived Node.js process handling many requests; it does not apply to a client-only SPA. - Read `package.json` first to confirm which Vue major and SSR toolchain are in use (`vue-server-renderer` for Vue 2, `@vue/server-renderer` / a meta-framework like Nuxt for Vue 3) — the per-request app/store creation requirement and the exact API names differ by major version and toolchain; do not apply Vue 3 API names to a Vue 2 codebase or vice versa. - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label the claim `documentation-based, unverified against current release`. ## Lean operating rules - Cross-request state pollution and injection findings default to HIGH severity. This is a security-scoped skill: do not downgrade a structural cross-request-pollution risk or an untraced `v-html` sanitizer gap to MEDIUM just because it has not been observed exploited yet — the risk is in the structure, not in whether someone has already hit it. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this might leak state" or "this v-html might be unsafe" without showing the specific module-scope declaration, the specific reachability path, or the specific unsanitized data-flow trace is not a valid finding — it is a guess. - Do not treat every module-scope declaration as a pollution risk. An immutable, non-reactive constant (a route table, a static config object, a compiled template) declared at module scope is safe. Only *mutable or reactive* state reachable from an SSR-rendered component's render path is the risk — check both properties (mutability/reactivity, and reachability) before flagging. - Do not approve a `v-html` binding whose data source includes any user-reachable input (route params, query strings, request bodies, third-party API responses that themselves echo user input) unless a named sanitizer call (e.g., DOMPurify) is visibly present on that exact data-flow path. A sanitizer import existing elsewhere in the codebase does not clear this bar — trace the specific path under review. - Check dynamic `:href`/`:src` bindings for scheme validation (an allowlist rejecting `javascript:` and other non-`http(s)` schemes) whenever the bound value's source includes user-reachable input. An unvalidated dynamic URL binding fed by user input is a MEDIUM-to-HIGH finding depending on reachability from an authenticated or public surface. - Watch for per-request factory functions that appear correct (a fresh `createApp()`/`createSSRApp()` call per request) but still close over a shared module-level cache, singleton, or default parameter passed in from outer scope — the factory pattern alone does not guarantee isolation if it references mutable shared state from its enclosing scope. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only). - Load only the reference needed for the concern in scope. ## References Load these only when needed: - [Review workflow and findings contract](references/workflow-and-output.md) — use for the step-by-step review procedure, the state-pollution/injection decision tree, and the required output shape. - [Cross-request state pollution](references/cross-request-state-pollution.md) — load only when reviewing an SSR entry point, tracing app/store/router creation, or investigating a suspected cross-request data-leak symptom. - [Injection: v-html and dynamic URL bindings](references/injection-and-dynamic-urls.md) — load only when the review scope includes a `v-html` usage or a dynamic `:href`/`:src` binding. Includes the OWASP XSS grounding reference; load that citation only when a `v-html` finding is actually present. ## Response minimum Return, at minimum: - the SSR entry point(s), module-scope declarations, and/or template bindings in scope, - ranked findings with file:line evidence, defect category (`state-pollution`, `xss`, or `url-injection`), the concrete data-flow trace (module-scope declaration and its reachability, or the origin-to-sink path for the injection), and a fix sketch matching Vue's documented pattern, - for every `v-html` finding, an explicit statement of whether a sanitizer call is present on the traced path — never approve on the assumption one exists elsewhere, - evidence level per finding (`repo evidence`, `documentation-based`, or `inference`), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block), - open questions or scope the review could not cover (e.g., "confirming actual cross-request leakage requires concurrent-request load testing, not static review").
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.