Claude Cursor GitHub Copilot Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download vincentchuwaichow-vanguard-frontier-agentic-skills_frontend_vue-state-store-security-review-febe32a.zip · 21 KB
Part of vincentchuwaichow/vanguard-frontier-agentic — 293 skills

Install

skills CLI npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/vue-state-store-security-review
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
Git 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-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 — 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-persistedstate configuration, 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, 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").
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.

No comments yet.

Reviews (0)

No reviews yet.

Related