vue-state-store-security-review
Statically review Pinia and legacy Vuex state stores for sensitive data persisted to localStorage/sessionStorage without scoping, untrusted server-payload hydration (window.__pinia/__INITIAL_STATE__) with un-escaped state serialization, SSR store-singleton cross-request pollution
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/vue-state-store-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 State Store Security Review
Purpose
Review Pinia and legacy Vuex state-store code for the security-critical defect classes that
are specific to state stores as a component — not general Vue architecture, not composable
design, and not SSR hydration/injection issues that belong to vue-ssr-security-review. This
skill exists to keep the review anchored to five documented defect classes: (a) sensitive data
persisted client-side without scoping, (b) store hydration from an untrusted server payload
with un-escaped serialization, (c) SSR store-singleton cross-request pollution, (d) store
plugins/hooks acting on untrusted payloads and client-held flags used as an authorization
source of truth, and (e) devtools state exposure in production. It does not re-litigate
general reactivity/composable architecture, v-html/URL-injection review (covered by
vue-ssr-security-review), or Composition API design quality (covered by
vue-composition-api-architecture-review) in every response.
When to use
Use this skill when the user asks to:
- review a Pinia store definition (
defineStore) or a legacy Vuex module/store for security issues, - assess whether
pinia-plugin-persistedstate/vuex-persistedstateconfiguration is safe (what gets persisted, and where), - review SSR store creation/hydration code (
entry-server.js, a Nuxt server plugin, an Express/Node handler that constructscreatePinia()/createStore()) for cross-request pollution risk, - review a store plugin,
$subscribe/$onActionhook, or Vuex plugin that consumes mutation/action payloads, - investigate whether a client-side role/permission flag (
isAdmin,role) is being trusted as an authorization boundary, - perform a pre-launch security review of a Pinia/Vuex-based application's state layer.
Do not use this skill for:
v-html/dynamic-URL injection review or SSR entry-point app/router creation — usevue-ssr-security-reviewfor those; this skill covers the store specifically, not the broader SSR rendering surface (load both skills together if the review spans both),- Composition API/composable architecture quality with no security angle — use
vue-composition-api-architecture-reviewinstead, - a general "which state-management library should we pick" architecture decision with no
security defect in scope — use
state-management-decision-reviewinstead, - a bug that requires live traffic reproduction (concurrent-request capture, a devtools screen-recording of production, live token exfiltration) to confirm exploitation — static analysis proves the structural risk, not that it has already been exploited.
Context7 Documentation Protocol
- Resolve library IDs with
resolve-library-idbefore citing any store-behavior claim. This skill's three grounding libraries:/vuejs/pinia(Pinia core — SSR hydration, plugins,$subscribe/$onAction),/prazdevs/pinia-plugin-persistedstate(persistence defaults andpick/storageconfig),/vuejs/vuex(legacy Vuex —stateas function,devtoolsoption, plugin/module API). - Use
query-docsagainst/prazdevs/pinia-plugin-persistedstateto confirm persistence defaults before flagging a persistence config:storagedefaults tolocalStoragewhen unset;pick(an array of dotted state-path strings) restricts persistence to named paths; with nopick, the entire state is persisted. Cite these asdocumentation-based. - Use
query-docsagainst/vuejs/piniato confirm SSR hydration mechanics before flagging a hydration finding: the documented client-side pattern ispinia.state.value = JSON.parse(window.__pinia), and Pinia's own SSR guide states escaping the serialized state is "VERY important if the content of the state can be changed by the user, which is almost always the case," recommendingdevalue(or equivalent) over naiveJSON.stringify. Cite asdocumentation-based. - Use
query-docsagainst/vuejs/piniato confirm$subscribe/$onActionhook signatures (mutation object withtype/storeId/payload; action object withname/store/args/after/onError) before describing a plugin-hook finding — do not invent a hook name or argument Context7 does not confirm. - Use
query-docsagainst/vuejs/vuexto confirm thestate: () => ({...})factory pattern (module reusability without shared state) and thedevtools: booleanstore option before citing either — do not assume Pinia has an equivalentdevtoolsboolean ondefineStore/createPinia; Context7 does not confirm that API surface for Pinia, so any Pinia-devtools concern must be scoped to "a build-time devtools plugin explicitly force-enabled in production," not a Pinia store option, and labeledinferenceunless a repo-specific config is found. - Read
package.jsonfirst to confirm which store library is in play (pinia,pinia-plugin-persistedstate,vuex,vuex-persistedstate) and its major version — API names and defaults differ between Pinia and Vuex and across Vuex 3/4; do not apply one library's API names to the other. - 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
- Findings in every defect class below default to HIGH severity: unscoped sensitive-data persistence, untrusted/un-escaped state hydration, SSR store-singleton pollution, untrusted payload handling in store plugins/hooks, client-side-flag-as-authorization, and production devtools exposure. Do not downgrade a structural finding to MEDIUM because it has not been observed exploited — the risk is in the structure.
- Trace every finding to a concrete file:line and a concrete data-flow path. "This store might
leak sensitive data" or "this hydration looks risky" without naming the specific
persist/pick/storageconfig, the specific module-scopecreatePinia()/createStore()declaration, or the specific untraced payload sink is not a valid finding — it is a guess. - Before flagging a persistence config, read the full
state()shape and the fullpersistconfig together. Apersist: true(orpersist: {}) with nopick/pathsarray persists the entire state — treat every sensitive field in that state as persisted unless apicklist demonstrably excludes it. Apicklist that omits the sensitive field(s) clears the finding for those fields specifically (not for the whole store, if other sensitive fields remain unscoped). - Before flagging SSR store creation as cross-request pollution, confirm mutability/reachability
the same way
vue-ssr-security-reviewrequires for app instances: is thecreatePinia()/createStore()call inside the per-request handler, and does that handler close over any module-scope mutable/reactive reference? An immutable module-scope constant (a frozen config object, a static route table) is not the risk; a store instance or mutable cache is. - Do not approve a client-side role/permission flag (
isAdmin,role,permissions) as an authorization boundary for a mutating action unless the review also confirms (via visible server-side code, or an explicit statement that server-side authorization is out of scope for this static review) that the actual mutation is re-checked server-side. A store flag gating only UI visibility (hiding a button) is not itself a finding; a store flag gating whether a mutating network call is made is a finding regardless of UI-layer intent, because the flag is client-writable. - Do not treat every
$subscribe/$onAction/Vuex-plugin hook as risky. Only hooks that act on the payload with a side effect (write, external call, log of sensitive data, mutate another store) and lack payload validation are findings; read-only observation (e.g., analytics event naming) is not. - 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 decision tree across all five defect classes, and the required output shape.
- Client-side persistence and hydration — load when
reviewing
pinia-plugin-persistedstate/vuex-persistedstateconfiguration, SSR store creation/singleton risk, or server-payload hydration (window.__pinia/__INITIAL_STATE__). - Untrusted payloads and authorization —
load when reviewing
$subscribe/$onAction/Vuex-plugin hooks, client-held role/permission flags used for access control, or devtools production exposure. - Acceptance rubric — the enumerated defect/false-positive list this skill's rules were authored against; load if you need the underlying catch list rather than the operating rules derived from it.
Response minimum
Return, at minimum:
- the store definition(s), persistence config, SSR entry point(s), and/or plugin/hook code in scope,
- ranked findings with file:line evidence, defect category (
persistence,hydration,ssr-pollution,untrusted-payload,client-auth-flag, ordevtools-exposure), the concrete data-flow trace (state field → persist config, or server payload → hydration call, or module-scope declaration → per-request reachability, or payload → sink), and a fix sketch matching the grounding library's documented pattern, - for every persistence finding, an explicit statement of which state fields are covered by
pick/paths(if any) and which are not, - for every client-side-flag finding, an explicit statement of whether a server-side authorization re-check was found, not found, or is out of scope for this static review,
- 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," or "v-html/URL-injection review of this
same app is out of scope for this skill — see
vue-ssr-security-review").
Files (vanguard-frontier-agentic)
-
references
-
acceptance-rubric.md 6.7 KB
# Acceptance Rubric (write before the rest of the skill — this is the failing test) This rubric enumerates every defect a correct Vue state-store (Pinia / legacy Vuex) security review MUST catch, and the benign patterns it MUST NOT flag. `SKILL.md` and the other references are only "done" once every numbered item below is covered by an explicit operating rule, decision-tree branch, or reference section. `selfCheck` in the final structured output must map each item to the rule/reference that satisfies it. ## MUST CATCH 1. **Sensitive data persisted via `pinia-plugin-persistedstate` / `vuex-persistedstate` with no `pick`/`paths` restriction.** A store whose `state` includes an auth token, refresh token, session identifier, or PII field, and whose `persist: true` (or `persist: {}` with no `pick`/`paths` array) config persists the *entire* state object — the sensitive field rides along by default. `storage` defaults to `localStorage`, which is readable by any script in the page's origin (XSS-exfiltratable, no `HttpOnly`-equivalent protection). 2. **Sensitive data persisted to `localStorage` explicitly (not just by default) when `sessionStorage` or a narrower `pick` would suffice.** E.g. `storage: localStorage` combined with a `pick` list that still includes `token`/`accessToken`/`user.ssn`/etc. 3. **Store hydration from an untrusted/unvalidated server payload.** Client code that reads `window.__pinia`, `window.__INITIAL_STATE__`, or an equivalent global and feeds it directly into `pinia.state.value = JSON.parse(...)` (or assigns into a Vuex store's state) with no schema/shape validation and no evidence the server-side serialization step escaped the value before embedding it in the HTML response. 4. **Un-escaped state serialization into the HTML response on the server side.** Server entry code that builds the embedded state string via naive `JSON.stringify(state)` interpolated directly into a `<script>` block (e.g. a template literal like `` `<script>window.__pinia=${JSON.stringify(state)}</script>` ``) with no escaping of `<`, `>`, `/`, or use of a safe-serialization library (`devalue` or equivalent) — a stored value containing `</script><script>alert(1)</script>` breaks out of the tag and executes. 5. **SSR store singleton created once at module scope instead of freshly per request.** A `const pinia = createPinia()` (or `const store = new Vuex.Store({...})` / `createStore({...})` assigned to a module-level `const`/`let`) declared at the top level of an SSR entry file (`entry-server.js`/`.ts`, a Nuxt server plugin, an Express/Node request handler module) and reused/imported across requests instead of being constructed inside the per-request handler function. This leaks one user's cart/auth/session state into another concurrent user's response. 6. **A per-request store factory that still closes over module-scope mutable/reactive state.** The factory function itself calls `createPinia()`/`createStore()` fresh each invocation, but also reads or writes a module-level cache, singleton, or mutable default parameter from enclosing scope — the factory call looking correct does not prove isolation. 7. **`$subscribe` / `$onAction` plugin hooks (or a Vuex plugin) that consume `mutation`/`args` payloads and act on them (write to a DB, call an external API, log, mutate other stores) without validating/sanitizing the payload shape or origin** — especially when the store/action is reachable from a component that accepts route params, query strings, or other user-controlled input as the action's argument. 8. **Client-side store flags (`isAdmin`, `role`, `isPremium`, `permissions`) used as an authorization source of truth for gating a sensitive action or UI-triggered mutation**, with no corresponding server-side authorization check on the actual mutating request. A client can set/patch its own store state (via devtools, `$patch`, or direct console access), so `if (store.isAdmin) { await deleteUser(id) }` with no server-side re-check is a broken access-control finding, not merely a UX nicety. 9. **Devtools integration left enabled in a production build.** Vuex's `devtools: true` (or an unset/default-true devtools option) shipped in a production store configuration exposes full state-tree contents, mutation/action history, and time-travel debugging to anyone with browser devtools open — a state/PII exposure risk if the store holds sensitive data. (Pinia's devtools integration is dev-only-by-default at build time via the Vue devtools plugin; flag only if a codebase explicitly force-enables it in a production config — do not invent a `devtools` option on Pinia's `defineStore`/`createPinia` APIs, which Context7 does not confirm exists.) ## MUST NOT FLAG (explicit false-positive guards) 10. **An immutable, non-sensitive constant persisted intentionally.** A `pick`-scoped persistence of a UI preference (`theme`, `locale`, `sidebarCollapsed`) with no auth/session/PII content is not a finding, even though it uses `localStorage` — state this explicitly as reviewed-and-cleared rather than omitting it silently. 11. **A store correctly re-created per request.** An SSR entry point whose `createPinia()` (or `createStore()`) call sits inside the exported per-request handler function, with no closed-over mutable module-scope state, is not a finding — do not flag the mere presence of `createPinia()` in an SSR file without checking its enclosing scope. 12. **Server-sent state that is properly escaped/serialized (`devalue` or equivalent) before embedding, and validated/schema-checked on the client before hydration.** Not a finding — state this explicitly as reviewed. 13. **`$subscribe`/`$onAction` hooks used for read-only observation** (e.g. analytics logging of a mutation type, a devtools-only debug logger) with no write/mutating side effect and no unsanitized use of the payload in a sink — not a finding, but note it was reviewed. 14. **A client-side role/flag used only for non-authoritative UI purposes** (e.g. hiding a menu item) where the actual protected action is independently re-authorized server-side — not a finding for that specific action, though the review should still confirm the server check exists rather than assuming it. ## Traceability requirement Every finding above must cite a concrete file:line and the exact data-flow path (store declaration → persistence config / hydration call / SSR entry scope / action-payload sink / authorization check) — a finding that says "this store might leak" without naming the specific `persist`/`pick`/`storage` config, the specific module-scope declaration, or the specific untraced sink is not valid per this skill's traceability rule. -
persistence-and-hydration.md 11 KB
# Client-Side Persistence and Server-to-Client Hydration Use this reference when reviewing `pinia-plugin-persistedstate`/`vuex-persistedstate` configuration, SSR store creation/singleton risk, or server-payload hydration (`window.__pinia`/`__INITIAL_STATE__`). ## Defect (a): sensitive data persisted to localStorage/sessionStorage without scoping ### What people get wrong The naive assumption is: > "I added `persist: true` so refreshes don't lose the user's cart — that's just a convenience > feature, not a security decision." Wrong. `persist: true` (Pinia) persists the *entire* store state to `storage` by default, and `pinia-plugin-persistedstate`'s `storage` option itself defaults to `localStorage` (`documentation-based`, via Context7 `/prazdevs/pinia-plugin-persistedstate`). If that store also holds an auth token, a refresh token, a session identifier, or PII — which is common when the same store that holds "is the user logged in" convenience state also holds the token used to answer that question — the token rides along into `localStorage`, a plain string store readable by any script running in the page's origin. Unlike an `HttpOnly` cookie, there is no mechanism preventing JavaScript (including injected/XSS script) from reading it. ### Officially grounded rules - `pinia-plugin-persistedstate`'s `storage` option accepts `localStorage` (default), `sessionStorage`, or a custom storage object implementing `getItem`/`setItem` (`documentation-based`). - The `pick` option (an array of dotted state-path strings, e.g. `['save.me', 'saveMeToo']`) restricts persistence to only the named paths; **with no `pick`, the entire state object is persisted** (`documentation-based`). Legacy Vuex's `vuex-persistedstate` exposes the equivalent restriction via its `paths` option. - A single store can use multiple persistence configs (an array of `{ pick, storage }` objects) to route different fields to different storage — e.g. a non-sensitive UI field to `localStorage` and nothing sensitive persisted at all (`documentation-based`). ### Review procedure 1. Read the full `state()` shape of the store. 2. Read the full `persist` config (or absence of one). If `persist` is absent, there is no persistence finding for this store regardless of what it contains. 3. If `persist: true` or `persist: {}` (no `pick`/`paths`) → every field in `state()` is persisted. Cross-reference against the sensitivity classification (auth/session/PII vs. UI-preference/non-sensitive). 4. If `persist: { pick: [...] }` → only the named paths are persisted. Check whether any sensitive field is named in that list. If none are, the persistence finding does not apply to this store (note that in the review) — but confirm no sensitive field was missed by checking the full list against the full `state()` shape, not just skimming for the word "token". 5. Check `storage`. Note explicitly whether it is `localStorage` (default, worse for sensitive data) or `sessionStorage`/a custom secure storage adapter — but do not treat `sessionStorage` alone as clearing a finding for a sensitive field; both are readable by same-origin script, meaning both are XSS-exfiltratable. The `storage` choice affects the window of exposure (tab lifetime vs. persistent), not whether the data is exposed to XSS at all. ### Minimal safe pattern ```ts // Only the non-sensitive UI preference is persisted; the auth token is never // scoped into `pick`, so it never reaches storage. export const useAuthStore = defineStore('auth', { state: () => ({ token: '', // sensitive — intentionally NOT in `pick` below refreshToken: '', // sensitive — intentionally NOT in `pick` below theme: 'light', // non-sensitive UI preference }), persist: { pick: ['theme'], storage: sessionStorage, }, }) ``` ### Anti-pattern (do not approve) ```ts export const useAuthStore = defineStore('auth', { state: () => ({ token: '', refreshToken: '', user: { email: '', ssn: '' }, }), persist: true, // WRONG: entire state, including token/refreshToken/ssn, persisted // to localStorage (the plugin's default storage) with no `pick`. }) ``` ## Defect (b): store hydration from an untrusted server payload ### What people get wrong The naive assumption is: > "The state blob came from our own server, so it's trusted — `JSON.parse` is safe, and > embedding it with `JSON.stringify` is just plumbing." Wrong in two distinct ways, both documented directly by Pinia's own SSR guide (`documentation-based`, via Context7 `/vuejs/pinia`): 1. **Serialization-side (server → HTML):** naively interpolating `JSON.stringify(pinia.state.value)` into an HTML `<script>` block is unsafe if any part of that state can be influenced by user-submitted content (a bio field, a comment, any value a user or another user previously submitted that flowed into the store) — which is "almost always the case." A crafted value containing `</script><script>...` breaks out of the tag and executes as script in every subsequent visitor's browser. Pinia's docs explicitly recommend a safe-serialization library (`devalue` or equivalent) and call escaping "**VERY important**." 2. **Hydration-side (client parse):** the documented client pattern is `pinia.state.value = JSON.parse(window.__pinia)` — this assigns the *entire* parsed object directly into the live store state with no schema/shape check. If the value was tampered with in transit, or the serialization step itself was compromised, this is a direct trust-without-validation of a value that traveled through the DOM. ### Review procedure 1. Find the server-side code that serializes store state for hydration. Grep for `JSON.stringify(` combined with a variable referencing `pinia.state`, a Vuex store's `state`, or an equivalent root-state accessor, especially where the result is concatenated into an HTML template string or a `<script>` tag. 2. Determine whether that serialization uses a safe-serialization library (`devalue` or equivalent) or applies explicit escaping of `<`, `>`, `/` before embedding. If it is a bare `JSON.stringify` with no escaping step anywhere in the call chain → **HIGH** finding. 3. Find the client-side hydration code — grep for `window.__pinia`, `window.__INITIAL_STATE__`, or an equivalent global, and the assignment into `pinia.state.value` (or a Vuex store's `replaceState`/direct state assignment). 4. Check whether the parsed value is validated (a schema check, a shape guard, a runtime type check) before being assigned/used, or is trusted as-is. Trusting as-is is a finding whenever the state's contents could have been influenced by any user's prior input — treat this as the default assumption unless the review can show the state is fully server-computed with no user-submitted content anywhere upstream. ### Minimal safe pattern ```js // server: escape before embedding import devalue from 'devalue' // ... after rendering, pinia.state.value holds this request's root state const serialized = devalue(pinia.state.value) // escapes for safe HTML embedding // embed `serialized` in the response, e.g. `<script>window.__pinia=${serialized}</script>` ``` ```js // client: hydrate only after confirming the shape looks like what's expected const raw = JSON.parse(window.__pinia) if (isValidPiniaStateShape(raw)) { pinia.state.value = raw } ``` ### Anti-pattern (do not approve) ```js // server — WRONG: naive stringify with no escaping, and no validation downstream const html = `<script>window.__pinia=${JSON.stringify(pinia.state.value)}</script>` ``` ```js // client — WRONG: entire parsed payload trusted with no shape/schema check pinia.state.value = JSON.parse(window.__pinia) ``` ## Defect (c): SSR store singleton cross-request pollution ### What people get wrong The naive assumption is: > "Each HTTP request gets its own function call, so a module-level `const pinia = > createPinia()` is fine — it's just one variable." Wrong. A Node.js SSR process is long-lived and handles many concurrent requests over shared module memory. A `createPinia()`/`createStore()` call sitting at module scope (executed once when the module is first `import`ed) is shared by every request the process ever handles after that — one user's cart, auth state, or session data written into that store during request A's handling can be read back during request B's handling, concurrently or on a subsequent request. ### Officially grounded rules - Pinia's SSR pattern is to construct a fresh `createPinia()` per request and pass it into a freshly created app instance for that request; the state is retrieved from that specific instance's `pinia.state.value` after rendering (`documentation-based`, via Context7 `/vuejs/pinia`). - Pinia's own "outside-component usage" guidance states explicitly that in SSR apps, the pinia instance must be passed explicitly to `useStore()` calls "to prevent the unintended sharing of global state between different application instances during the server-side rendering process" (`documentation-based`). - Vuex's module-reusability pattern requires `state` to be declared as a factory function (`state: () => ({...})`) rather than a plain object literal, specifically so that each module instance gets its own isolated state object rather than sharing one object reference (`documentation-based`, via Context7 `/vuejs/vuex`) — the same "fresh instance per use" principle Pinia's SSR guidance requires at the store-instance level. ### Review procedure 1. Grep the SSR entry file(s) for `createPinia(`, `createStore(`, `new Vuex.Store(` and determine the enclosing scope of each call site: module top level (executes once at import time) vs. inside an exported/invoked per-request handler function. 2. If found at module scope and reused across requests → **HIGH** finding, `ssr-pollution`. 3. If found inside a per-request handler, read the full body of that handler function and list every variable it references from enclosing/module scope. Classify each as immutable (safe) or mutable/reactive (risk). A per-request `createPinia()` call that also reads/writes a module-level cache or default object is still a finding — name the specific closed-over reference. 4. For Vuex modules intended to be reusable/instantiated more than once, confirm `state` is a function, not a plain object literal — a plain object literal shared across module instantiations is the Vuex-specific version of this same defect class. ### Minimal safe pattern ```js // entry-server.js export async function render(url) { const pinia = createPinia() // fresh per request const app = createApp(App) app.use(pinia) // ...populate state for this request only, then render... return { html, piniaState: pinia.state.value } } ``` ### Anti-pattern (do not approve) ```js // entry-server.js — WRONG: created once at module load, shared across every request const pinia = createPinia() export async function render(url) { const app = createApp(App) app.use(pinia) // every request shares the SAME pinia instance's state // ... } ``` -
untrusted-payloads-and-authorization.md 9.8 KB
# Untrusted Payloads, Client-Side Authorization Flags, and Devtools Exposure Use this reference when reviewing `$subscribe`/`$onAction` (Pinia) or plugin/module code (Vuex) that consumes mutation/action payloads, when checking whether a client-held role/flag is being used as an authorization boundary, or when checking devtools configuration for production exposure. ## Defect (d), part 1: store plugins / $subscribe / $onAction acting on untrusted payloads ### What people get wrong The naive assumption is: > "This is my own store's hook — the payload came from my own action, so it's already trusted > by the time it reaches the hook." Wrong when the action's argument itself originated from user-controlled input (a route param, a form field, a query string, a request body forwarded into the action call) and the hook does something with side effects. Pinia's `$subscribe` and `$onAction` hooks are generic instrumentation points — they receive whatever `mutation`/`args` content the calling code passed, with no built-in sanitization or validation (this is an architectural fact, not a defect in Pinia itself: the hook is a generic observation/interception point and the trust boundary is defined entirely by the caller). ### Officially grounded API shape (do not invent beyond this) Confirmed via Context7 `/vuejs/pinia` (`documentation-based`): - `store.$subscribe((mutation, state) => {...})` — the callback receives a `mutation` object (`mutation.type`: `'direct' | 'patch object' | 'patch function'`, `mutation.storeId`, `mutation.payload` for patch-object mutations) and the current `state`. - `store.$onAction(({ name, store, args, after, onError }) => {...})` — the callback receives the action `name`, the `store` instance, an `args` array of the parameters passed to the action, and `after`/`onError` hooks for post-action handling. - Vuex plugins receive the `store` instance directly (`store => {...}`) and can subscribe to mutations/actions via `store.subscribe`/`store.subscribeAction`; a namespaced module's action context includes `rootState`/`rootGetters`, giving plugin/action code reach across module boundaries (`documentation-based`, via Context7 `/vuejs/vuex`). Do not describe a hook argument, method, or option beyond what is listed above — if a reviewed codebase appears to use something not confirmed here, treat the claim about its *documented* behavior as unverified and say so, rather than asserting a specific contract. ### Review procedure 1. Enumerate every `$subscribe`, `$onAction` (Pinia), or `store.subscribe`/ `store.subscribeAction`/plugin registration (Vuex) in scope. 2. For each, read the hook body. Classify: does it only observe (log a mutation type, emit an analytics event with non-sensitive metadata) or does it act (write to a DB, call an external API, mutate a *different* store, log payload content that could be sensitive)? 3. For hooks that act, trace the payload's (`mutation.payload` / `args`) origin backward to the action call site. Does the action's argument ever originate from user-controlled input (a route param, a form value, a request body) with no validation between that input and the hook's consumption of it? 4. If yes and the hook has a side effect on unvalidated content → **HIGH** finding, `untrusted-payload`. Name the specific hook, the specific action/mutation, and the specific unvalidated hop. 5. If the hook is read-only or the payload is already validated/sanitized before the hook consumes it → not a finding; state this explicitly. ### Anti-pattern (do not approve) ```ts // A plugin acts on unvalidated action args reachable from a route param. pinia.use(({ store }) => { store.$onAction(({ name, args }) => { if (name === 'updateProfile') { // args[0] flows from a route param with no validation upstream — // writing it straight to an external audit-log API with no sanitization. auditLogApi.post('/log', { field: args[0].bio }) } }) }) ``` ## Defect (d), part 2: client-held flags used as an authorization source of truth ### What people get wrong The naive assumption is: > "The store says `isAdmin: true` only after the server told us the user is an admin, so > checking `store.isAdmin` in the client before calling the delete endpoint is safe." Wrong. Once a value lives in client-side reactive state, it is client-writable — via browser devtools, a `$patch()` call from the console, direct manipulation of a hydrated store, or a compromised/malicious browser extension. A store flag is a UI convenience for *display* decisions; it is never proof of authorization for a *mutating* decision. The only correct authorization boundary is the server independently re-checking the authenticated user's permissions when the mutating request actually arrives. ### Review procedure 1. Grep for role/permission-flag reads gating a mutating call: patterns like `if (store.isAdmin)`, `if (userStore.role === 'admin')`, `v-if="store.canDelete"` guarding an action dispatch or a direct API call that performs a write (delete, update, privilege change, financial transaction). 2. For each match, determine what the guarded code actually does: - Gates only UI visibility (hides/shows a button, disables a form field) with the underlying mutating action's authorization enforced elsewhere → not a finding for that specific gate, but confirm (or explicitly flag as unconfirmed) that the actual mutating endpoint re-authorizes server-side. - Gates whether the mutating network call is *made at all*, with no evidence the server independently re-checks authorization on that request → **HIGH** finding, `client-auth-flag`, regardless of how the flag was originally populated. 3. If the server-side code implementing the endpoint is not in scope/not provided, say so explicitly as an open question rather than assuming a check exists — do not clear the finding on the assumption that "surely the backend checks this too." ### Anti-pattern (do not approve) ```ts // store.isAdmin is client-side reactive state — devtools-patchable. async function deleteUser(id: string) { if (userStore.isAdmin) { // WRONG: client flag treated as the auth boundary await api.delete(`/users/${id}`) // if the endpoint doesn't re-check, this is a } // full broken-access-control vulnerability } ``` ### Minimal safe pattern ```ts // Client-side check is UX-only (avoids showing an error after the fact); // the actual authorization decision is made server-side on every request. async function deleteUser(id: string) { if (userStore.isAdmin) { // Optimistic UX gate only — the server independently re-verifies the // caller's role/permissions before performing the deletion. await api.delete(`/users/${id}`) } } ``` Note: the client code above is nearly identical in shape to the anti-pattern — the difference is entirely in the server-side endpoint, which is why this defect class requires confirming (or explicitly flagging as unconfirmed) the server-side check rather than judging the client code in isolation. ## Defect (e): devtools state exposure in production builds ### What people get wrong The naive assumption is: > "Devtools only matter in development — production builds strip that stuff automatically." Not reliably true without an explicit build-time step, and Vuex exposes an *explicit runtime option* that can force devtools integration on regardless of environment. ### Officially grounded rule Vuex's store options API documents a `devtools: boolean` option that "activates or deactivates devtools integration for a Vuex instance," called out specifically as useful when running multiple stores on a single page (`documentation-based`, via Context7 `/vuejs/vuex`). If a production build's store configuration sets `devtools: true` (or leaves a config path that defaults to enabling it reachable in production), the full state tree, mutation/action history, and time-travel debugging are exposed to anyone with browser devtools open — a meaningful exposure if the store holds any sensitive data (tokens, PII, internal flags). Context7 does not confirm an equivalent `devtools` boolean option on Pinia's `defineStore`/ `createPinia` APIs — Pinia's devtools integration is wired through the Vue devtools browser extension/plugin rather than a store-constructor option. Do not assert a Pinia-specific `devtools` config option exists. If a codebase under review appears to force-enable a devtools integration in a production build (e.g., an explicit plugin registration reachable in the production bundle), flag that as a repo-evidence finding scoped to the specific code found — do not generalize it into a claim about a Pinia API that Context7 does not confirm. ### Review procedure 1. Grep store-configuration files for a `devtools:` option (Vuex) and determine whether the value is `true`, absent (check documented default for the version in use), or explicitly tied to an environment check (`process.env.NODE_ENV !== 'production'` or equivalent). 2. If `devtools: true` (or an unguarded default) is reachable in a production build path → **HIGH** finding, `devtools-exposure`. 3. If gated behind an environment check that correctly excludes production → not a finding; state this explicitly. 4. For Pinia codebases, check only for an explicit, repo-specific forced-enable of a devtools plugin in a production bundle path — do not invent or assume a `devtools` constructor option exists on `createPinia()`/`defineStore()`. ### Minimal safe pattern ```js // Vuex — devtools explicitly disabled outside development const store = createStore({ // ... devtools: process.env.NODE_ENV !== 'production', }) ``` ### Anti-pattern (do not approve) ```js // Vuex — WRONG: devtools unconditionally enabled, including in production builds const store = createStore({ // ... devtools: true, }) ``` -
workflow-and-output.md 9.8 KB
# Review Workflow and Findings Contract Use this reference for the step-by-step review procedure and the required output shape. Load the domain references only for the specific defect class the store code under review actually raises. ## Prerequisites - Identify the store library and version in use (`package.json` — `pinia` + `pinia-plugin-persistedstate`, or `vuex` + `vuex-persistedstate`/a custom persistence plugin). API names and defaults differ between the two; do not apply one library's names to the other. - Identify whether the app is SSR (an `entry-server.js`/`.ts`, a Nuxt server context, or equivalent request-handling entry point exists). If not SSR, defect class (c) — SSR singleton pollution — and the server-side half of defect class (b) — un-escaped serialization — are out of scope; state this explicitly rather than silently skipping them. ## Workflow 1. **Locate every store definition** (`defineStore(...)` for Pinia, `createStore(...)`/ `new Vuex.Store(...)`/module objects for Vuex). For each, read the full `state` shape. 2. **Classify every state field by sensitivity.** Auth/refresh tokens, session identifiers, API keys, PII (name, email, address, government ID), and any field whose disclosure would aid session hijacking or identity theft are sensitive. UI preferences, non-sensitive display config, and derived/computed-only values are not. 3. **Trace persistence configuration for every store with a `persist` option (Pinia) or a `vuex-persistedstate`/custom persistence plugin registration (Vuex).** For each, determine: `storage` target (`localStorage` default vs. `sessionStorage` vs. custom), and whether a `pick`/`paths` restriction excludes every sensitive field identified in step 2. See `references/persistence-and-hydration.md`. 4. **Trace SSR store creation** (if SSR). Determine whether `createPinia()`/`createStore()` is invoked inside the per-request handler function or at module scope, and whether the per-request factory closes over any module-scope mutable/reactive reference. See `references/persistence-and-hydration.md`. 5. **Trace server-to-client state hydration** (if SSR). Find the server-side serialization of store state into the HTML response (e.g. embedding `JSON.stringify(pinia.state.value)` or `devalue(...)` output in a `<script>` block) and the client-side consumption (`pinia.state.value = JSON.parse(window.__pinia)` or equivalent). Determine whether the serialization step escapes the value for safe HTML embedding, and whether the client applies any validation before hydrating. See `references/persistence-and-hydration.md`. 6. **Enumerate every store plugin, `$subscribe`, `$onAction` (Pinia), or Vuex plugin/module registration.** For each, trace what the hook does with the mutation/action payload — does it write, call an external API, log, or mutate another store based on unvalidated payload content? See `references/untrusted-payloads-and-authorization.md`. 7. **Search for client-side role/permission flags used to gate a mutating action** — grep for patterns like `if (store.isAdmin)`, `if (userStore.role === ...)` guarding a network call or a store action that performs a write, and check whether the corresponding server endpoint independently re-authorizes. See `references/untrusted-payloads-and-authorization.md`. 8. **Check devtools configuration** for a production build — a Vuex `devtools: true` (or unset, since default behavior should be checked against the version in use) in a config file reachable by a production build step, or any explicit repo-specific override that force-enables a devtools integration in production. See `references/untrusted-payloads-and-authorization.md`. 9. **Produce ranked findings** using the output contract below. ## Decision tree - Store `state` includes a sensitive field (token/session-id/PII) AND `persist` has no `pick`/`paths` restriction excluding it (or `persist: true`/`persist: {}`) → **HIGH** finding, category `persistence`. Note the `storage` target explicitly (localStorage is worse than sessionStorage, but both are XSS-exfiltratable — do not treat `sessionStorage` alone as clearing the finding for a sensitive field). - Store `state` includes a sensitive field AND `pick`/`paths` demonstrably excludes every sensitive field → not a finding for those fields; state this explicitly. - Store `state` contains only non-sensitive fields (UI prefs, non-PII display config) → not a finding regardless of persistence config; state this explicitly rather than omitting the store from the review. - Server embeds serialized store state in the HTML response using naive `JSON.stringify` interpolation with no escaping and no `devalue`/equivalent safe-serialization call → **HIGH** finding, category `hydration` (XSS via state-serialization breakout). - Client hydrates `pinia.state.value = JSON.parse(window.__pinia)` (or Vuex equivalent) with no shape/schema validation of the parsed result before use → **HIGH** finding, category `hydration` (trusting an untrusted payload), unless the value is proven fully server-controlled with no user-reachable content anywhere in its construction. - Server-side serialization is escaped (`devalue` or equivalent) and/or client-side hydration validates shape before use → not a finding; state this explicitly. - SSR entry creates `createPinia()`/`createStore()` at module scope, reused across requests → **HIGH** finding, category `ssr-pollution`. - SSR entry's per-request factory closes over a module-scope mutable/reactive reference (a cache, a singleton, a mutable default parameter) → **HIGH** finding, category `ssr-pollution` — name the specific closed-over reference. - SSR entry creates the store fresh inside the per-request handler with no closed-over mutable state → not a finding; state this explicitly. - A `$subscribe`/`$onAction`/Vuex-plugin hook performs a side effect (write/external call/sensitive log/cross-store mutation) using unvalidated payload content that is reachable from user-controlled input (route params, form input, an action argument sourced from a request) → **HIGH** finding, category `untrusted-payload`. - A `$subscribe`/`$onAction`/Vuex-plugin hook is read-only (observation, non-sensitive analytics logging) → not a finding; state this explicitly. - A client-side role/permission flag gates whether a mutating network call is made, and no server-side re-authorization is visible in the code under review → **HIGH** finding, category `client-auth-flag`. If server-side authorization code is out of scope/not provided, state this as an explicit open question rather than assuming it exists. - A client-side role/permission flag gates only UI visibility (hiding a button/menu item) with the actual mutating action separately confirmed to be re-authorized server-side → not a finding; state this explicitly. - Devtools integration is explicitly enabled (or left at a default that enables it) in a config path reachable by the production build → **HIGH** finding, category `devtools-exposure`. Do not invent a Pinia `devtools` store option — ground any Pinia-specific devtools concern in the build-time devtools plugin, and label as `inference` unless a concrete repo config is found; ground Vuex findings in the documented `devtools: boolean` store option. ## Output contract Every response from this skill must return: 1. **Scope** — the store definition(s), persistence config, SSR entry point(s), and/or plugin/hook code reviewed. 2. **Ranked findings** — each with file:line, defect category (`persistence` / `hydration` / `ssr-pollution` / `untrusted-payload` / `client-auth-flag` / `devtools-exposure`), the concrete data-flow trace (naming every hop), and a fix sketch matching the grounding library's documented pattern. 3. **Persistence coverage statement** — for every persistence finding, which fields are scoped out by `pick`/`paths` (if any) and which are not. 4. **Server-authorization statement** — for every client-auth-flag finding, whether a server-side re-check was found, not found, or is out of scope for this review. 5. **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. 6. **Verdict** — approve / approve-with-notes / block. 7. **Open questions or out-of-scope items** — e.g., "confirming actual cross-request leakage requires concurrent-request load testing," "server-side authorization for this action was not in the provided scope," or "v-html/URL-injection review of this same app is out of scope for this skill — see `vue-ssr-security-review`." ## When to push back Push back if the user asks to: - approve an unscoped `persist: true` on a store containing a token/session field because "we only persist it for convenience" — convenience is not a mitigant; the field must be excluded via `pick`/`paths` or persistence removed, - treat a client-side `isAdmin`/`role` flag as sufficient authorization because "the UI already hides the button" — a hidden button does not stop a direct API call or a devtools-patched store value; the server must re-check, - skip the SSR-singleton check because "we haven't seen cross-user leakage in production" — this defect class is structural and often invisible until concurrent load exposes it, - accept naive `JSON.stringify` state embedding as "probably fine since it's just our own data" — any state reachable from user-submitted content anywhere upstream needs escaping; assume user-reachability unless proven otherwise, - downgrade an untraced `$subscribe`/`$onAction` finding to informational because "it's probably fine" — this skill's default is HIGH for exactly this class of unproven claim.
-
-
metadata.json 2.3 KB
{ "id": "vue-state-store-security-review", "name": "Vue State Store Security Review", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Reviews Pinia and legacy Vuex state stores for sensitive data persisted to localStorage/sessionStorage without scoping, untrusted server-payload hydration (window.__pinia/__INITIAL_STATE__) with un-escaped state serialization, SSR store-singleton cross-request pollution, store plugins/$subscribe/$onAction acting on untrusted payloads, client-held role flags used as an authorization source of truth, and devtools state exposure in production builds, grounding claims via Context7 and each library's own documentation.", "source_type": "original", "official_docs": [ "https://pinia.vuejs.org/ssr/", "https://pinia.vuejs.org/core-concepts/plugins.html", "https://github.com/prazdevs/pinia-plugin-persistedstate/blob/main/docs/guide/config.md", "https://vuex.vuejs.org/guide/modules.html", "https://vuex.vuejs.org/api/", "https://owasp.org/www-project-top-ten/", "https://owasp.org/www-community/attacks/xss/" ], "security_notes": "This skill's entire scope is security-critical: unscoped client-side persistence of tokens/PII is an XSS-exfiltratable data-exposure vector, untrusted/un-escaped state hydration is an XSS and data-integrity vector, SSR store-singleton pollution is a cross-tenant/cross-user data-exposure defect, untrusted payload handling in store plugins/hooks can drive unsanitized writes or calls, client-held role flags used as an authorization source of truth is a broken-access-control defect, and production devtools exposure leaks the full state tree to any user with browser devtools open. Every finding in this skill defaults to HIGH severity unless proven otherwise with concrete evidence (a pick/paths scope, an escaping call, a per-request creation trace, a server-side re-check). Static-review-only skill: it reads and greps store definitions, persistence config, SSR entry points, and plugin/hook code but never executes, builds, or runs application code, and never sends live requests.", "last_verified": "2026-07-03", "path": "skills/frontend/vue-state-store-security-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 11 KB
--- name: vue-state-store-security-review description: Statically review Pinia and legacy Vuex state stores for sensitive data persisted to localStorage/sessionStorage without scoping, untrusted server-payload hydration (window.__pinia/__INITIAL_STATE__) with un-escaped state serialization, SSR store-singleton cross-request pollution, store plugins/$subscribe/$onAction acting on untrusted payloads, client-held role flags used as an authorization source of truth, and devtools state exposure in production builds. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-03" category: security --- # Vue State Store Security Review ## Purpose Review Pinia and legacy Vuex state-store code for the security-critical defect classes that are specific to *state stores* as a component — not general Vue architecture, not composable design, and not SSR hydration/injection issues that belong to `vue-ssr-security-review`. This skill exists to keep the review anchored to five documented defect classes: (a) sensitive data persisted client-side without scoping, (b) store hydration from an untrusted server payload with un-escaped serialization, (c) SSR store-singleton cross-request pollution, (d) store plugins/hooks acting on untrusted payloads and client-held flags used as an authorization source of truth, and (e) devtools state exposure in production. It does not re-litigate general reactivity/composable architecture, v-html/URL-injection review (covered by `vue-ssr-security-review`), or Composition API design quality (covered by `vue-composition-api-architecture-review`) in every response. ## When to use Use this skill when the user asks to: - review a Pinia store definition (`defineStore`) or a legacy Vuex module/store for security issues, - assess whether `pinia-plugin-persistedstate` / `vuex-persistedstate` configuration is safe (what gets persisted, and where), - review SSR store creation/hydration code (`entry-server.js`, a Nuxt server plugin, an Express/Node handler that constructs `createPinia()`/`createStore()`) for cross-request pollution risk, - review a store plugin, `$subscribe`/`$onAction` hook, or Vuex plugin that consumes mutation/action payloads, - investigate whether a client-side role/permission flag (`isAdmin`, `role`) is being trusted as an authorization boundary, - perform a pre-launch security review of a Pinia/Vuex-based application's state layer. Do not use this skill for: - `v-html`/dynamic-URL injection review or SSR entry-point app/router creation — use `vue-ssr-security-review` for those; this skill covers the *store* specifically, not the broader SSR rendering surface (load both skills together if the review spans both), - Composition API/composable architecture quality with no security angle — use `vue-composition-api-architecture-review` instead, - a general "which state-management library should we pick" architecture decision with no security defect in scope — use `state-management-decision-review` instead, - a bug that requires live traffic reproduction (concurrent-request capture, a devtools screen-recording of production, live token exfiltration) to confirm exploitation — static analysis proves the structural risk, not that it has already been exploited. ## Context7 Documentation Protocol - Resolve library IDs with `resolve-library-id` before citing any store-behavior claim. This skill's three grounding libraries: `/vuejs/pinia` (Pinia core — SSR hydration, plugins, `$subscribe`/`$onAction`), `/prazdevs/pinia-plugin-persistedstate` (persistence defaults and `pick`/`storage` config), `/vuejs/vuex` (legacy Vuex — `state` as function, `devtools` option, plugin/module API). - Use `query-docs` against `/prazdevs/pinia-plugin-persistedstate` to confirm persistence defaults before flagging a persistence config: `storage` defaults to `localStorage` when unset; `pick` (an array of dotted state-path strings) restricts persistence to named paths; with no `pick`, the entire state is persisted. Cite these as `documentation-based`. - Use `query-docs` against `/vuejs/pinia` to confirm SSR hydration mechanics before flagging a hydration finding: the documented client-side pattern is `pinia.state.value = JSON.parse(window.__pinia)`, and Pinia's own SSR guide states escaping the serialized state is "**VERY important** if the content of the state can be changed by the user, which is almost always the case," recommending `devalue` (or equivalent) over naive `JSON.stringify`. Cite as `documentation-based`. - Use `query-docs` against `/vuejs/pinia` to confirm `$subscribe`/`$onAction` hook signatures (mutation object with `type`/`storeId`/`payload`; action object with `name`/`store`/`args`/ `after`/`onError`) before describing a plugin-hook finding — do not invent a hook name or argument Context7 does not confirm. - Use `query-docs` against `/vuejs/vuex` to confirm the `state: () => ({...})` factory pattern (module reusability without shared state) and the `devtools: boolean` store option before citing either — do not assume Pinia has an equivalent `devtools` boolean on `defineStore`/ `createPinia`; Context7 does not confirm that API surface for Pinia, so any Pinia-devtools concern must be scoped to "a build-time devtools plugin explicitly force-enabled in production," not a Pinia store option, and labeled `inference` unless a repo-specific config is found. - Read `package.json` first to confirm which store library is in play (`pinia`, `pinia-plugin-persistedstate`, `vuex`, `vuex-persistedstate`) and its major version — API names and defaults differ between Pinia and Vuex and across Vuex 3/4; do not apply one library's API names to the other. - 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 - Findings in every defect class below default to HIGH severity: unscoped sensitive-data persistence, untrusted/un-escaped state hydration, SSR store-singleton pollution, untrusted payload handling in store plugins/hooks, client-side-flag-as-authorization, and production devtools exposure. Do not downgrade a structural finding to MEDIUM because it has not been observed exploited — the risk is in the structure. - Trace every finding to a concrete file:line and a concrete data-flow path. "This store might leak sensitive data" or "this hydration looks risky" without naming the specific `persist`/`pick`/`storage` config, the specific module-scope `createPinia()`/`createStore()` declaration, or the specific untraced payload sink is not a valid finding — it is a guess. - Before flagging a persistence config, read the full `state()` shape and the full `persist` config together. A `persist: true` (or `persist: {}`) with no `pick`/`paths` array persists the entire state — treat every sensitive field in that state as persisted unless a `pick` list demonstrably excludes it. A `pick` list that omits the sensitive field(s) clears the finding for those fields specifically (not for the whole store, if other sensitive fields remain unscoped). - Before flagging SSR store creation as cross-request pollution, confirm mutability/reachability the same way `vue-ssr-security-review` requires for app instances: is the `createPinia()`/ `createStore()` call inside the per-request handler, and does that handler close over any module-scope mutable/reactive reference? An immutable module-scope constant (a frozen config object, a static route table) is not the risk; a store instance or mutable cache is. - Do not approve a client-side role/permission flag (`isAdmin`, `role`, `permissions`) as an authorization boundary for a mutating action unless the review also confirms (via visible server-side code, or an explicit statement that server-side authorization is out of scope for this static review) that the actual mutation is re-checked server-side. A store flag gating only UI visibility (hiding a button) is not itself a finding; a store flag gating whether a mutating network call is *made* is a finding regardless of UI-layer intent, because the flag is client-writable. - Do not treat every `$subscribe`/`$onAction`/Vuex-plugin hook as risky. Only hooks that act on the payload with a side effect (write, external call, log of sensitive data, mutate another store) and lack payload validation are findings; read-only observation (e.g., analytics event naming) is not. - 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 decision tree across all five defect classes, and the required output shape. - [Client-side persistence and hydration](references/persistence-and-hydration.md) — load when reviewing `pinia-plugin-persistedstate`/`vuex-persistedstate` configuration, SSR store creation/singleton risk, or server-payload hydration (`window.__pinia`/`__INITIAL_STATE__`). - [Untrusted payloads and authorization](references/untrusted-payloads-and-authorization.md) — load when reviewing `$subscribe`/`$onAction`/Vuex-plugin hooks, client-held role/permission flags used for access control, or devtools production exposure. - [Acceptance rubric](references/acceptance-rubric.md) — the enumerated defect/false-positive list this skill's rules were authored against; load if you need the underlying catch list rather than the operating rules derived from it. ## Response minimum Return, at minimum: - the store definition(s), persistence config, SSR entry point(s), and/or plugin/hook code in scope, - ranked findings with file:line evidence, defect category (`persistence`, `hydration`, `ssr-pollution`, `untrusted-payload`, `client-auth-flag`, or `devtools-exposure`), the concrete data-flow trace (state field → persist config, or server payload → hydration call, or module-scope declaration → per-request reachability, or payload → sink), and a fix sketch matching the grounding library's documented pattern, - for every persistence finding, an explicit statement of which state fields are covered by `pick`/`paths` (if any) and which are not, - for every client-side-flag finding, an explicit statement of whether a server-side authorization re-check was found, not found, or is out of scope for this static review, - 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," or "v-html/URL-injection review of this same app is out of scope for this skill — see `vue-ssr-security-review`").
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.