Claude Cursor GitHub Copilot Skill

vue-router-navigation-security-review

Statically review Vue Router navigation guards, redirect flows, and template bindings for client-side guards used as the sole authorization boundary, open redirects via route.query.redirect/returnUrl, javascript:/data: scheme injection through dynamic :to/:href bindings, route pa

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-router-navigation-security-review-febe32a.zip · 20 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-router-navigation-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 Router Navigation Security Review

Purpose

Review Vue Router navigation guards, redirect flows, dynamic link bindings, and route configuration for the security-critical defect classes specific to client-side routing as a security surface — not general route-tree architecture, not component design, and not the state-store or SSR-hydration concerns owned by sibling skills. This skill exists to keep the review anchored to five documented defect classes: (a) a beforeEach/beforeEnter guard used as the sole authorization boundary with no server-side enforcement behind it, (b) open redirect via route.query.redirect/returnUrl passed unvalidated into router.push()/ window.location, (c) javascript:/data: scheme injection via a dynamic :to/:href binding, (d) route.params/route.query interpolated into v-html/innerHTML (reflected XSS through the router), and (e) next(unvalidatedPath)/guard-returned redirects creating loops or bypasses, overly-broad catch-all routes, and history-mode server misconfiguration. It does not re-litigate general route-tree/code-splitting architecture, Pinia/Vuex store security, or SSR cross-request pollution in every response.

When to use

Use this skill when the user asks to:

  • review a router.beforeEach/beforeEnter guard (or a Nuxt definePageMeta/route-middleware equivalent) that gates access to a route,
  • audit a login/logout flow's post-authentication redirect handling,
  • assess whether a <router-link :to="..."> or dynamic :href binding is safe from scheme injection,
  • investigate whether route.params/route.query reaching a v-html or .innerHTML sink is exploitable,
  • review a route table for redirect-loop risk, catch-all overreach, or history-mode server-fallback exposure,
  • perform a pre-launch security review of a Vue Router-based application's navigation layer.

Do not use this skill for:

  • general route-tree structure, loader/code-splitting, or focus-management review with no security angle — use routing-navigation-review instead (and note it targets React Router/Next.js, not Vue Router),
  • Pinia/Vuex store security (persisted sensitive data, SSR store-singleton pollution, untrusted hydration payloads, client-held role flags as an authorization source) — use vue-state-store-security-review instead,
  • SSR entry-point cross-request state pollution or general v-html/dynamic-URL injection unrelated to router data — use vue-ssr-security-review instead; this skill's injection scope is specifically route.params/route.query reaching a sink, not every v-html in the app,
  • broad token-storage, cookie-flag, CSRF, or OAuth/OIDC flow review with no router-specific mechanism in play — use frontend-auth-session-security-review instead; this skill covers only the router-mediated open-redirect mechanism (route.query.redirect/returnUrl), not session/token architecture generally,
  • confirming a bypass has already been exploited in production (concurrent-request reproduction, live penetration testing, a captured attack) — static analysis proves the structural risk, not that it has already been exploited.

Context7 Documentation Protocol

  • Resolve the Vue Router library ID with resolve-library-id (matched result: /vuejs/router, Vue Router's own source-and-docs repository) before citing any guard-mechanism, redirect-API, or dynamic-matching claim.
  • Use query-docs against /vuejs/router to corroborate: navigation-guard return semantics (beforeEach/beforeEnter returning false/a location/undefined, and the legacy next argument's call-exactly-once contract), the documented to.name !== 'Login' self-exclusion pattern, redirect function shapes (redirect: to => ({ path, query })), route.params/ route.query exposure with no built-in escaping, router.push's acceptance of bare string paths, createWebHistory()'s server-fallback requirement, and the /:pathMatch(.*)* catch-all pattern. These are all repo evidence claims — cite the specific guide file (navigation-guards, redirect-and-alias, dynamic-matching, history-mode) alongside the claim.
  • The conclusion that a client-side guard must not be the sole authorization boundary is not itself a Vue-Router API fact — Vue Router's docs describe guards strictly as navigation control flow (cancel/redirect/proceed), never as an access-control mechanism. Label this specific conclusion inference, grounded in general server-side-enforcement security practice (documentation-based via OWASP), layered on the repo evidence fact that guards are browser-executed callbacks.
  • The claim that v-html bypasses Vue's default template auto-escaping is general Vue template-compiler behavior, not a Vue-Router API — ground it in the Vue security guide (documentation-based, listed in this skill's official_docs), not in /vuejs/router.
  • Read package.json first to confirm the Vue Router major version in use — guard signatures (return-value guards vs. the legacy next third argument), redirect object shape, and catch-all syntax (/:pathMatch(.*)* in v4 vs. legacy * in v3) differ across majors; do not apply v4 API names to a v3 codebase or vice versa.
  • If Context7 is unavailable, fall back to the official_docs URLs in this skill's metadata.json and label the claim documentation-based, unverified against current release.

Lean operating rules

  • Every finding in this skill's scope defaults to HIGH severity: broken client-side-only access control, open redirect, scheme injection, and reflected XSS are all directly exploitable without further chaining. Do not downgrade to MEDIUM because "it hasn't been reported exploited yet" — the risk is structural.
  • Trace every finding to a concrete file:line and a concrete data-flow path (guard → endpoint gap, or origin → sink hops for injection/redirect findings). A finding that says "this guard might not be enough" or "this redirect might be exploitable" without the specific trace is a guess, not a finding.
  • Never flag "a beforeEach/beforeEnter guard exists" by itself. The finding requires the absence of confirmed server-side enforcement on the endpoint the protected route depends on — look for evidence (a 401/403 check, a BFF layer, an API contract note) before concluding it's missing, and state explicitly what you found or didn't find.
  • Never accept a type check or substring blocklist as clearing an open-redirect or scheme-injection finding. The only acceptable control is an allowlist: same-origin/ relative-path validation for redirects, and an explicit protocol allowlist (http:/https:/mailto:) for dynamic link bindings.
  • Do not approve a v-html/.innerHTML sink fed by route.params/route.query unless a named sanitizer call is visibly present on that exact traced path — a sanitizer existing elsewhere in the codebase does not clear this bar.
  • Check every unconditional redirect-on-guard for the framework's own documented self-exclusion pattern (to.name !== 'Login' or equivalent). Its absence is a HIGH redirect-loop finding, not a style nit — Vue Router's docs treat it as required.
  • Read the full routes array in registration order before clearing a catch-all route; a broad/catch-all pattern registered before a route that should require auth can shadow it.
  • 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:

Response minimum

Return, at minimum:

  • the guard(s), redirect flow(s), dynamic link binding(s), route table, and/or v-html/ innerHTML sink(s) in scope,
  • ranked findings with file:line evidence, defect category (client-guard-as-sole-authz / open-redirect / scheme-injection / reflected-xss / redirect-loop / catch-all-misconfig / history-mode-server-misconfig), the concrete data-flow trace (the guard-to-endpoint gap, or the full origin-to-sink path naming every hop), and a fix sketch matching Vue Router's documented pattern,
  • for every guard finding, an explicit statement of whether server-side enforcement evidence was found and where — never infer it exists without evidence on the traced path,
  • evidence level per finding (repo evidence, documentation-based, or inference), with structural-risk findings explicitly labeled as structural risk, not confirmed-exploited,
  • verdict (approve / approve-with-notes / block),
  • open questions or scope the review could not cover (e.g., "confirming the API layer actually rejects unauthorized requests requires a live request or reading server-side code outside this diff's scope").
Files (vanguard-frontier-agentic)
  • references
    • acceptance-rubric.md 7.3 KB
      # Acceptance Rubric (write first, satisfy second)
      
      This is the failing test for this skill. Every item below must be traceable to a specific
      operating rule or reference file in this skill before the skill is considered complete. The
      `selfCheck` returned by the authoring agent must map each item to what satisfies it.
      
      ## MUST CATCH — concrete defects a correct review of this domain must flag
      
      1. **Client-side guard as sole authorization boundary.** A `router.beforeEach`/`beforeEnter`
         (or a meta-framework's `definePageMeta`/middleware equivalent) checks `isAuthenticated` or a
         role flag and redirects if false/absent, but no server-side check exists on the
         corresponding API/data-fetch call the protected route triggers. Reachable/exploitable by:
         directly calling the underlying API endpoint (curl, browser devtools fetch) while bypassing
         the SPA entirely, or by disabling JavaScript / patching the bundled guard function in
         devtools before navigation resolves. The guard only ever gates the client-side route
         render; the server has no independent memory of "this user passed the guard."
      
      2. **Open redirect via `route.query.redirect`/`returnUrl`.** A login flow reads
         `to.query.redirect` (or `route.query.returnUrl`) and passes it unvalidated into
         `router.push(redirectTarget)` or `window.location.href = redirectTarget` after
         authentication succeeds. Reachable by: crafting a login URL such as
         `/login?redirect=https://evil.example.com` (or a protocol-relative `//evil.example.com`),
         which sends an authenticated user's session to an attacker-controlled origin post-login —
         a classic phishing/token-leak primer, since the URL often still carries a valid session
         token or referrer header to the destination.
      
      3. **`javascript:`/`data:` scheme injection via dynamic `:to`/`:href`.** A `<router-link :to="...">`
         or a plain `<a :href="...">` built from user-reachable input (route params, query string,
         API response echoing user content) is bound without scheme validation. Reachable by:
         submitting a value like `javascript:alert(document.cookie)` or a `data:text/html;base64,...`
         payload as the "profile link"/"website" field, which executes when a victim clicks the
         rendered link.
      
      4. **Route params/query interpolated into `v-html`/`innerHTML` (reflected XSS through the
         router).** A component reads `route.params.x` or `route.query.q` and renders it via
         `v-html` (or manually assigns `.innerHTML`) with no sanitizer call on that exact path.
         Reachable by: a crafted URL like `/search?q=<img src=x onerror=alert(1)>` shared with a
         victim — the payload never touches a database; it reflects straight from the URL into the
         rendered DOM.
      
      5. **`next(unvalidatedPath)` / guard-returned redirect creating a loop or bypass.** A
         `beforeEach` guard redirects unconditionally (e.g., every unauthenticated user to `/login`)
         without excluding the target route itself (`to.name !== 'Login'`), producing an infinite
         redirect loop; or a guard's redirect target is itself built from unvalidated `to.query`/
         `to.params`, letting a crafted URL redirect through/around the intended destination.
      
      6. **Overly-broad catch-all route with no security review.** A `{ path: '/:pathMatch(.*)*' }`
         (or legacy `*`) catch-all route is wired to a component or redirect that assumes it only
         ever receives "not found" traffic, but the component actually renders `route.params.pathMatch`
         unsanitized, or the catch-all silently matches and serves a path that should have 404'd
         (e.g., shadowing a more specific route that was supposed to require auth).
      
      7. **`history` mode (`createWebHistory`) with missing/incorrect server SPA fallback.** The app
         uses `createWebHistory()` (clean URLs) but the review has no evidence the server is
         configured to serve `index.html` for unmatched paths — either producing a broken
         direct-link/refresh experience, or (the security-relevant variant) exposing raw source/
         config files at paths the SPA never intended to be routable because the server's fallback
         rule is too permissive (e.g., serving `index.html` for `/api/*` or static-asset-looking
         paths that should 404 or hit a different handler).
      
      ## MUST NOT FLAG — benign patterns that must not produce a finding
      
      8. A `beforeEach`/`beforeEnter` guard used purely for UX redirection (e.g., "send anonymous
         users to `/login` so they don't see a blank protected page") **when the review has evidence
         the corresponding server endpoint/API independently enforces authorization** (a 401/403 on
         direct call, a documented server-side check, or a BFF that re-validates the session). This
         is the correct pattern, not a finding — do not flag "client guard exists" alone as a defect;
         flag only the *absence* of the server-side counterpart.
      
      9. A redirect target that is a static, hardcoded, same-origin path (e.g.,
         `redirect: { name: 'Login' }` or `router.push('/dashboard')`) — no user input reaches the
         destination, so there is no open-redirect surface regardless of how the redirect is
         triggered.
      
      10. A redirect that reads `to.query.redirect`/`returnUrl` but **validates it against an
          allowlist of same-origin relative paths** (e.g., confirms the value starts with a single
          `/` and is not protocol-relative `//`, or resolves it with `new URL(value, window.location.origin)`
          and confirms the resolved origin matches) before using it. Validated same-origin redirects
          are the documented-safe pattern, not a finding.
      
      11. A `:to`/`:href` binding whose value is a compile-time literal, a named-route object
          (`{ name: 'user', params: { id } }`), or a value confirmed to have already passed scheme
          allowlisting — do not re-flag a binding that already has a visible scheme check on its
          exact data-flow path.
      
      12. `route.params`/`route.query` values consumed only via text interpolation (`{{ }}`), passed
          to `router.push`'s own `path`/`name`/`params` fields, used in non-rendering logic (API
          query params, conditional branching), or rendered through a sanitizer call on the traced
          path — Vue's default template interpolation auto-escapes; only the `v-html`/`innerHTML`
          sink is the defect, not the mere presence of `route.params`/`route.query` in a component.
      
      13. A `beforeEach` guard with an explicit self-exclusion check (`to.name !== 'Login'`, or
          equivalent) before redirecting — the documented loop-avoidance pattern is correctly
          applied; do not flag it as a residual risk without a concrete path that still loops.
      
      14. A catch-all route (`/:pathMatch(.*)*`) that renders only a static "not found" component
          with no rendering of `route.params.pathMatch` and no broader-than-intended matching
          behavior (confirmed by reading the actual route table ordering) — this is the documented,
          correct catch-all pattern.
      
      15. `createWebHistory()` paired with confirmed server-side SPA-fallback configuration (e.g., a
          reviewed nginx `try_files`/framework adapter config that serves `index.html` only for
          non-asset, non-API paths) — not a finding; state it as reviewed-and-safe.
      
      ## Mapping requirement
      
      Every one of items 1–7 must be covered by a decision-tree rule in
      `references/workflow-and-output.md` and a detailed rule in one of the two domain reference
      files. Every one of items 8–15 must appear as an explicit "not a finding" branch in the same
      places — silence is not sufficient; the skill must say *why* the benign pattern is not flagged.
      
    • client-guards-and-control-flow.md 9.6 KB
      # Client-Side Guards as an Authorization Boundary, and Navigation Control-Flow Risks
      
      Use this reference when the review scope includes a `router.beforeEach`/`beforeEnter` (or a
      meta-framework's route-middleware equivalent), any redirect returned from a guard, a catch-all
      route, or the app's `history` mode configuration. Covers rubric items 1, 5, 6, 7 (and their
      "not a finding" counterparts 8, 13, 14, 15).
      
      ## What people get wrong
      
      The naive assumption:
      
      > "This route checks `isAuthenticated` in `beforeEach` and redirects to `/login` if it's false,
      > so the route is protected."
      
      Wrong in isolation. Vue Router's own guard mechanics confirm exactly what a guard is: a
      JavaScript callback that runs *in the browser*, before a navigation is allowed to resolve. It
      can return `false` to cancel the navigation, a route location to redirect, or (with the legacy
      third argument) call `next()` — all of this is client-side control flow (`repo evidence`,
      `/vuejs/router`, navigation-guards guide). None of it executes on a server the attacker doesn't
      control. A guard is the correct place for *UX* gating — don't render a protected page's shell
      to a user who hasn't logged in — but it is not, and cannot be, the mechanism that stops a
      request from reaching protected data, because:
      
      - the bundled JavaScript (and therefore the guard's logic) is fully visible and can be read,
        patched via devtools, or skipped by calling the underlying API/data-fetch directly;
      - `isAuthenticated` itself is usually just client-held state (a token's presence, a decoded JWT
        claim, a Pinia/Vuex flag) that the guard reads — it is not an independent judgment the server
        makes on the attacker's actual request.
      
      ## Officially grounded requirement
      
      Vue Router's documented guard pattern for authentication redirects is:
      
      ```js
      router.beforeEach(async (to, from) => {
        if (
          !isAuthenticated &&
          // Avoid an infinite redirect
          to.name !== 'Login'
        ) {
          return { name: 'Login' }
        }
      })
      ```
      
      (`repo evidence`, `/vuejs/router`, navigation-guards guide — including the documented
      `to.name !== 'Login'` self-exclusion check, which is the framework's own prescribed loop-avoidance
      pattern, not an optional hardening step.) The docs also show the same pattern via `meta` fields:
      
      ```js
      router.beforeEach((to, from) => {
        if (to.meta.requiresAuth && !auth.isLoggedIn()) {
          return { path: '/login', query: { redirect: to.fullPath } }
        }
      })
      ```
      
      (`repo evidence`, `/vuejs/router`, meta guide.) Nowhere in Vue Router's own documentation is a
      guard described as an authorization mechanism that replaces server-side access control — the
      docs frame it strictly as navigation control flow (cancel / redirect / proceed). The conclusion
      that a guard must not be the *sole* boundary is standard web-security practice (client-side
      controls are trivially bypassable; the server must independently authorize every request) rather
      than a Vue-Router-specific API claim — treat this specific conclusion as `inference` grounded in
      `documentation-based` general security guidance (OWASP: never rely on client-side enforcement
      alone), layered on top of the `repo evidence` fact that guards are browser-executed callbacks.
      
      Guards can also programmatically extend the route table and redirect in one motion:
      
      ```js
      router.beforeEach(to => {
        if (!hasNecessaryRoute(to)) {
          router.addRoute(generateRoute(to))
          return to.fullPath
        }
      })
      ```
      
      (`repo evidence`, `/vuejs/router`, dynamic-routing guide.) If `generateRoute()` builds a route
      pattern from untrusted input, the pattern itself becomes an injection surface — treat this the
      same as any other user-controlled routing input.
      
      ## Non-negotiable design rules
      
      ### 1. A guard finding requires evidence of an ABSENT server-side check, not just a present client check
      
      Do not flag "there is a client-side guard" as the defect. The finding is: this route's guard
      exists, **and** the review found no evidence that the API/data endpoint the route depends on
      independently checks authorization on the server. Look for: a 401/403 test or comment, a BFF
      layer, server middleware, or an API contract doc showing enforcement. If such evidence exists,
      the guard is correctly UX-only and this is not a finding (rubric item 8).
      
      ### 2. Distinguish "guard exists with server enforcement" from "guard is believed to be enough"
      
      Ask explicitly, in the finding write-up, whether the codebase (or the user) asserts the guard
      *is* the security boundary ("the API trusts anyone who reached this route") versus treats it as
      UX convenience layered over real server checks. The former is the HIGH finding; the latter,
      with visible server-side evidence, is not.
      
      ### 3. Redirect self-exclusion prevents the documented loop failure mode
      
      Every unconditional-redirect guard (`if (!isAuthenticated) return { name: 'Login' }` with no
      exclusion for the login route itself) is a HIGH finding: it produces an infinite redirect loop
      the moment the guard runs on the login route's own navigation. The fix is the framework's own
      documented pattern (`to.name !== 'Login'` or equivalent). A guard that already includes this
      check is correctly implemented (rubric item 13) — do not invent a residual risk without a
      concrete counter-path.
      
      ### 4. A guard-returned or `next()`-passed redirect target must itself be traced
      
      If the redirect location returned by a guard (or passed to `next(...)`) is built from
      `to.query`/`to.params` rather than a hardcoded route, trace that value the same way you would
      trace an open-redirect target (see `open-redirect-and-injection.md`) — a guard can reintroduce
      the exact same open-redirect defect class it was written to prevent.
      
      ### 5. Catch-all routes need their actual render path checked, not just their existence
      
      Vue Router's documented catch-all pattern is `{ path: '/:pathMatch(.*)*', name: 'NotFound',
      component: NotFound }` (`repo evidence`, `/vuejs/router`, dynamic-matching guide). This pattern
      alone is correct and not a finding. It becomes a finding only when: (a) the matched component
      renders `route.params.pathMatch` through an unsanitized sink, or (b) route table ordering
      causes the catch-all to shadow a route that should require authentication (read the full routes
      array in declaration order — Vue Router matches in the order routes are registered).
      
      ### 6. History-mode server configuration is out of the router's control but in scope for the review
      
      `createWebHistory()` (`repo evidence`, `/vuejs/router`, history-mode guide) produces
      clean URLs but requires the server to serve the app's `index.html` for any path the SPA should
      handle client-side. This is not itself a router-code defect, but the review must check for
      evidence of the corresponding server/proxy fallback rule (nginx `try_files`, a framework's SPA
      middleware, a static-host rewrite rule) — its *absence* breaks direct links/refreshes, and an
      *overly broad* fallback (matching `/api/*` or asset paths that should 404 or route elsewhere)
      can leak information or serve the app shell where a different, more restrictive handler was
      intended.
      
      ## Minimal safe pattern
      
      ```js
      // Guard: UX-only redirect, with the documented loop-avoidance check.
      router.beforeEach((to, from) => {
        if (to.meta.requiresAuth && !authStore.isAuthenticated && to.name !== 'Login') {
          return { name: 'Login', query: { redirect: to.fullPath } }
        }
      })
      
      // Server / BFF: the actual authorization boundary (illustrative — lives outside router code).
      // GET /api/account -> 401 if session invalid, regardless of any client-side navigation state.
      ```
      
      Anti-pattern (guard as the only boundary — do not approve without server-side evidence):
      
      ```js
      router.beforeEach((to, from) => {
        if (to.meta.requiresAuth && !authStore.isAuthenticated) {
          return { name: 'Login' } // no `to.name !== 'Login'` check -> loop risk
        }
        // No evidence anywhere in the reviewed diff/API that /api/account (or whatever
        // this route fetches) independently checks the session server-side.
      })
      ```
      
      ## Adversarial checklist
      
      - If a user disables JavaScript, or calls the route's underlying API directly with curl/devtools,
        does the server independently reject the unauthorized request? If unknown, ask — do not assume
        yes.
      - Does every unconditional redirect-on-guard have a self-exclusion for its own target route?
      - Is any guard's redirect destination built from `to.query`/`to.params` rather than a hardcoded
        name/path? If so, cross-reference `open-redirect-and-injection.md`.
      - Does `router.addRoute()` (if called from within a guard) build its route definition from any
        user-reachable input?
      - Read the full `routes` array in registration order — does a catch-all or broad pattern appear
        before a route that should take precedence and require auth?
      - Does `createWebHistory()` appear with no server config file in the reviewed diff/repo showing
        the SPA-fallback rule? Flag this as an open question if the server config is out of scope
        rather than assuming it is either correct or missing.
      
      ## Verification targets
      
      - Grep for `router.beforeEach(`, `beforeEnter:`, and meta-framework middleware files
        (`middleware/`, `definePageMeta`) and read each guard's full body.
      - Grep for `to.name !==`, `from.name !==`, or equivalent self-exclusion checks near a redirect
        return/`next(...)` call inside each guard found above.
      - Grep for `router.addRoute(` calls inside guard bodies and trace the argument's construction.
      - Grep for `pathMatch(.*)` / `path: '*'` and read the matched component's template for any
        `route.params.pathMatch` usage.
      - Grep for `createWebHistory(` and check the repo for a corresponding server config file
        (`nginx.conf`, `vercel.json`, `netlify.toml`, a framework's server entry) with a fallback rule;
        note explicitly if that file is outside the review's scope.
      
    • open-redirect-and-injection.md 9.2 KB
      # Open Redirect, Scheme Injection, and Reflected XSS Through the Router
      
      Use this reference when the review scope includes a post-login redirect flow reading
      `route.query`, a dynamic `:to`/`:href` binding, or a `v-html`/`innerHTML` sink fed by
      `route.params`/`route.query`. Covers rubric items 2, 3, 4 (and their "not a finding"
      counterparts 9, 10, 11, 12).
      
      ## What people get wrong
      
      The naive assumption, said three different ways depending on which defect it excuses:
      
      > "It's just redirecting back to where the user came from — that's a UX feature, not a
      > security decision."
      
      > "It's a router-link, Vue Router handles the URL, so it must be safe."
      
      > "It's just the search query showing what the user typed — of course it should render as-is."
      
      All three treat *router-adjacent* data (a `redirect` query param, a `:to` binding, a
      `route.params`/`route.query` value) as inherently trusted because it arrived through routing
      machinery rather than a form POST. Vue Router's own docs confirm the opposite: dynamic segments
      are "exposed... as `route.params`" with no escaping step described anywhere in the dynamic-
      matching guide (`repo evidence`, `/vuejs/router`) — they are raw strings lifted from the URL,
      exactly as attacker-controllable as any query-string or form value.
      
      ## Officially grounded facts
      
      - **Redirect functions receive the live route object and can echo it verbatim.** Vue Router's
        documented redirect patterns include function-based redirects that read `to.params`/`to.query`
        directly: `redirect: to => ({ path: '/search', query: { q: to.params.searchText } })` and
        `redirect: to => \`/redirected-path/${to.params.id}\`` (`repo evidence`, `/vuejs/router`,
        redirect-and-alias / extending-routes guides). The mechanism for building a redirect target
        from live route data is a first-class, documented feature — which is exactly why an
        *unvalidated* version of the same mechanism (echoing a full attacker-supplied URL instead of a
        path segment) is dangerous: the framework will not stop you.
      - **`router.push` accepts a bare string path with no origin/scheme validation of its own** —
        `router.push('/users/eduardo')`, `router.push(\`/user/${username}\`)` (`repo evidence`,
        `/vuejs/router`, navigation guide). If `username` (or any interpolated value) is instead a
        full external URL or a `javascript:` string, `router.push` does not reject it — it is the
        calling code's responsibility to validate before calling.
      - **`RouterLink`'s `:to` prop is commonly extended to bind directly to `:href` for external
        links, gated only by an `isExternalLink` string check** — Vue Router's own
        extending-router-link guide shows `:href="to"` used directly for values where
        `typeof props.to === 'string' && props.to.startsWith('http')` (`repo evidence`,
        `/vuejs/router`, extending-router-link guide). This is the documented pattern for handling
        external links alongside internal ones — but note precisely what it does *not* include: any
        scheme allowlist beyond the `http` prefix check. A value like `javascript:...` or
        `httpjavascript:` crafted to defeat a naive `startsWith('http')` check is not excluded by this
        documented example alone; production code needs an explicit protocol allowlist (see pattern
        below), not just an `http`-prefix string check.
      - **`route.params`/`route.query` carry no built-in HTML-escaping** — confirmed by the dynamic-
        matching guide's description of params as directly exposed URL segments (`repo evidence`,
        `/vuejs/router`). Vue's *template* interpolation (`{{ }}`) auto-escapes when you render these
        values normally; the risk is exclusively when a `route.params`/`route.query` value reaches a
        `v-html` binding or a manual `.innerHTML` assignment, which bypass that auto-escaping by
        design (`documentation-based`, Vue security guide — this specific `v-html`-is-unescaped claim
        is general Vue template-compiler behavior, not a Vue-Router API, so it is grounded in the
        `official_docs` Vue security guide rather than the `/vuejs/router` library).
      
      ## Non-negotiable design rules
      
      ### 1. Open redirect: validate against a same-origin/relative-path allowlist, not a blocklist
      
      The only acceptable fix for a `redirect`/`returnUrl` query value used in a post-auth redirect is
      allowlisting: confirm the value is a relative path starting with a single `/` (reject
      protocol-relative `//` and any value containing `://`), or resolve it with
      `new URL(value, window.location.origin)` and compare the resolved `origin` to the app's own
      origin. A denylist of "known bad" values (blocking `http://`/`https://` substrings, for example)
      is not sufficient — treat a blocklist-only approach as still a finding, since it is trivially
      bypassed by encoding or alternate schemes.
      
      ### 2. Scheme injection: allowlist protocols explicitly, don't rely on prefix checks alone
      
      For any dynamic `:to`/`:href` fed by user-reachable input, require a visible allowlist check
      (e.g., `['http:', 'https:', 'mailto:'].includes(new URL(value, base).protocol)`) on the exact
      data-flow path. A `startsWith('http')` check (as shown in Vue Router's own external-link
      extension example) is a routing-decision heuristic (is this internal or external), not a
      security control — do not accept it as clearing a scheme-injection finding.
      
      ### 3. Trace `v-html`/`innerHTML` sinks fed by route data back to the URL, not just to a variable name
      
      If a component does `<div v-html="route.query.bio">` or
      `el.innerHTML = route.params.description`, the origin is the URL itself — the most directly
      user-controlled input there is, requiring no stored/second-order step. Do not treat this as
      lower-severity than a stored-XSS path; a reflected payload via a shared link is exploitable
      immediately.
      
      ### 4. Text interpolation and non-rendering uses of route data are not the defect
      
      `{{ route.query.q }}`, `:aria-label="route.params.id"`, passing `route.params.id` into an API
      call, or using it as a `v-if` condition are all safe — Vue's default interpolation escapes, and
      non-DOM-rendering uses have no injection sink at all. Only flag the specific `v-html`/`innerHTML`
      sink, not every place a component touches `route.params`/`route.query`.
      
      ## Minimal safe implementation patterns
      
      Open redirect, validated:
      
      ```js
      const SAFE_REDIRECT = /^\/(?!\/)/ // single leading slash, not protocol-relative
      
      function safeRedirectTarget(raw) {
        return typeof raw === 'string' && SAFE_REDIRECT.test(raw) ? raw : '/'
      }
      
      router.push(safeRedirectTarget(route.query.redirect))
      ```
      
      Scheme-validated dynamic link:
      
      ```vue
      <script setup>
      const ALLOWED_SCHEMES = ['http:', 'https:', 'mailto:']
      
      function safeHref(url) {
        try {
          const parsed = new URL(url, window.location.origin)
          return ALLOWED_SCHEMES.includes(parsed.protocol) ? parsed.href : '#'
        } catch {
          return '#'
        }
      }
      </script>
      
      <template>
        <a :href="safeHref(profile.websiteUrl)">Website</a>
      </template>
      ```
      
      Route data into `v-html`, sanitized:
      
      ```vue
      <script setup>
      import DOMPurify from 'dompurify'
      import { computed } from 'vue'
      import { useRoute } from 'vue-router'
      
      const route = useRoute()
      const safeQueryPreview = computed(() => DOMPurify.sanitize(String(route.query.q ?? '')))
      </script>
      
      <template>
        <div v-html="safeQueryPreview"></div>
      </template>
      ```
      
      Anti-patterns (do not approve):
      
      ```js
      // Open redirect: unvalidated query value passed straight to router/window.
      router.push(route.query.redirect) // or: window.location.href = route.query.returnUrl
      ```
      
      ```vue
      <!-- Scheme injection: no protocol allowlist. -->
      <router-link :to="userProvidedProfileLink">Visit</router-link>
      ```
      
      ```vue
      <!-- Reflected XSS: route.query rendered via v-html with no sanitizer on the path. -->
      <div v-html="route.query.q"></div>
      ```
      
      ## Adversarial checklist
      
      - Does any code read `route.query.redirect`/`returnUrl` (or similarly named params) and pass it
        to `router.push`, `router.replace`, or `window.location` without an allowlist check?
      - Is the allowlist check a same-origin/relative-path confirmation, or only a substring blocklist
        (which does not count as a fix)?
      - Does any `:to`/`:href` binding's traced source include user-reachable input with no protocol
        allowlist visible on that exact path?
      - Would `javascript:`, `data:`, or `vbscript:` reach any such binding unmodified?
      - Does any `v-html` or manual `.innerHTML` assignment's traced source include
        `route.params`/`route.query` with no named sanitizer call on that exact path?
      - Is a redirect-loop or bypass reachable by feeding a guard's own redirect target from
        unvalidated `to.query`/`to.params` (cross-reference `client-guards-and-control-flow.md`)?
      
      ## Verification targets
      
      - Grep for `route.query.redirect`, `route.query.returnUrl`, `to.query.redirect`, and similar
        names; trace each into `router.push(`, `router.replace(`, or `window.location`.
      - Grep for `:to="` and `:href="` bound to a non-literal expression; trace each backward through
        props/computed/store to its origin.
      - Grep for `v-html` and `.innerHTML =` and trace each backward for any `route.params`/
        `route.query` origin.
      - Grep for a scheme/protocol allowlist (`ALLOWED_SCHEMES`, `new URL(`, `.protocol ===`) near
        each dynamic link binding found above, and confirm it sits on the traced path rather than
        merely existing elsewhere in the file.
      - Grep for a sanitizer import (`dompurify` or an equivalent project utility) and confirm its
        call site is on the traced `v-html`/`innerHTML` path.
      
    • workflow-and-output.md 9.1 KB
      # Review Workflow and Findings Contract
      
      Use this reference for the step-by-step review procedure and the required output shape. Load
      the two domain references only for the specific defect class the routing code under review
      actually raises.
      
      ## Prerequisites
      
      - Confirm Vue Router is actually in use (`package.json` — `vue-router`) and identify the major
        version. Guard signatures (`next` third argument vs. return-value guards), redirect object
        shape, and catch-all syntax (`/:pathMatch(.*)*` vs. legacy `*`) differ across Vue Router 3 vs.
        4 — do not apply v4 syntax expectations to a v3 codebase or vice versa.
      - Identify whether the app runs in `history` mode (`createWebHistory`) or hash mode
        (`createWebHistory` vs. `createWebHashHistory`) — the server-fallback concern
        (`client-guards-and-control-flow.md`, rule 6) only applies to `history` mode.
      - Identify whether this is a meta-framework (Nuxt) rather than raw Vue Router — Nuxt's
        `definePageMeta`/route middleware wraps the same underlying guard concepts but with different
        file-based conventions; note this explicitly rather than assuming `router.beforeEach` exists
        verbatim in a Nuxt app.
      
      ## Workflow
      
      1. **Locate every navigation guard.** Grep for `router.beforeEach(`, `beforeEnter:` on route
         definitions, and (for Nuxt) `middleware/` files or `definePageMeta({ middleware: ... })`.
         Read each guard's full body.
      2. **For each guard that gates a protected route, look for the server-side counterpart.**
         Identify what API/data endpoint the protected route depends on and search for evidence
         (tests, comments, a BFF layer, an API contract doc) that the endpoint independently enforces
         authorization server-side. Absence of such evidence is the finding — see
         `client-guards-and-control-flow.md`.
      3. **Trace every redirect.** For guard-returned redirects, `next(...)` calls, and any
         post-login redirect flow, determine: is the target hardcoded/same-origin, or built from
         `to.query`/`to.params`/`route.query`? If the latter, trace whether it is validated against a
         same-origin/relative-path allowlist before use. See `open-redirect-and-injection.md`.
      4. **Enumerate every dynamic `:to`/`:href` binding in scope.** Trace each backward through
         props, computed values, and store state to its origin. Check for a visible protocol
         allowlist on the exact traced path when the origin includes user-reachable input.
      5. **Enumerate every `v-html`/`innerHTML` sink in scope that touches `route.params`/
         `route.query`.** Trace backward the same way; check for a named sanitizer call on the exact
         path.
      6. **Check guard self-exclusion and route-table ordering.** For every unconditional
         redirect-on-guard, confirm a self-exclusion check exists for the guard's own redirect target.
         Read the full `routes` array in declaration order and check whether a catch-all or broad
         pattern could shadow a route that should require authentication.
      7. **Check `history`-mode server configuration when in scope.** If `createWebHistory()` is used,
         look for a server/proxy config file with an SPA-fallback rule; note explicitly if that file
         is outside the review's scope rather than assuming correctness.
      8. **Produce ranked findings** using the output contract below.
      
      ## Decision tree
      
      - A guard redirects unauthenticated users away from a protected route, and no evidence exists
        that the route's underlying API/data endpoint independently enforces authorization
        server-side → **HIGH** finding, `client-guard-as-sole-authz`. Cite Vue Router's own framing of
        guards as client-side navigation control flow (`repo evidence`) plus general server-side-
        enforcement practice (`inference`/`documentation-based`).
      - A guard redirects unauthenticated users away from a protected route, **and** the route's
        server endpoint is confirmed to independently reject unauthorized requests → not a finding;
        state explicitly that the guard is correctly UX-only.
      - A post-auth redirect flow passes `route.query.redirect`/`returnUrl` (or equivalent) into
        `router.push`/`window.location` with no same-origin/relative-path validation → **HIGH**
        finding, `open-redirect`.
      - The same flow validates the value against a same-origin/relative-path allowlist before use →
        not a finding.
      - A dynamic `:to`/`:href` binding's traced source includes user-reachable input with no
        protocol allowlist on the exact path → **HIGH** finding, `scheme-injection` (reachable via a
        crafted profile/link field triggering `javascript:`/`data:` execution on click).
      - The same binding's source is a literal, a named-route object, or already passes a protocol
        allowlist on the traced path → not a finding.
      - `route.params`/`route.query` reaches a `v-html` binding or manual `.innerHTML` assignment
        with no named sanitizer call on the exact traced path → **HIGH** finding, `reflected-xss`.
      - `route.params`/`route.query` is used only via text interpolation, non-rendering logic, or
        passes through a sanitizer call on the traced path → not a finding.
      - An unconditional redirect-on-guard has no self-exclusion check for its own redirect target →
        **HIGH** finding, `redirect-loop` (Vue Router's own docs prescribe the `to.name !== 'Login'`
        self-exclusion pattern as required, not optional).
      - The guard already includes a self-exclusion check → not a finding.
      - A guard's redirect target (or a `next(...)` argument) is itself built from unvalidated
        `to.query`/`to.params` → **HIGH** finding, `open-redirect` (the guard reintroduces the same
        defect class it exists to prevent) — cross-reference the open-redirect rule above.
      - A catch-all route (`/:pathMatch(.*)*`) renders `route.params.pathMatch` through an
        unsanitized sink, or route-table ordering lets it shadow a route that should require auth →
        **MEDIUM-to-HIGH** finding, `catch-all-misconfig`, depending on what is actually exposed.
      - A catch-all route renders only a static not-found component with no broader-than-intended
        matching (confirmed via route-table order) → not a finding.
      - `createWebHistory()` is used with no evidence of a corresponding server-side SPA-fallback
        rule in scope → note as an **open question**, not a confirmed finding, unless the server
        config is actually in the reviewed diff/repo and shown to be missing or overly broad, in
        which case it is a **MEDIUM** finding, `history-mode-server-misconfig`.
      - `createWebHistory()` is paired with a reviewed, correctly scoped server-fallback rule → not a
        finding; state it as reviewed-and-safe.
      
      ## Output contract
      
      Every response from this skill must return:
      
      1. **Scope** — the guard(s), redirect flow(s), dynamic link binding(s), route table, and/or
         `v-html`/`innerHTML` sink(s) reviewed.
      2. **Ranked findings** — each with file:line, defect category (`client-guard-as-sole-authz` /
         `open-redirect` / `scheme-injection` / `reflected-xss` / `redirect-loop` /
         `catch-all-misconfig` / `history-mode-server-misconfig`), the concrete data-flow trace (every
         hop from origin to sink, or the guard-to-endpoint gap for the authz-boundary category), and
         a fix sketch matching Vue Router's documented pattern.
      3. **Server-side enforcement status for every guard finding** — an explicit statement of
         whether evidence of independent server-side authorization was found, and where; never infer
         server enforcement exists without evidence on the traced path.
      4. **Evidence level per finding** — `repo evidence`, `documentation-based`, or `inference`.
         Label structural-risk findings (e.g., "no evidence of server enforcement") as structural
         risk, not confirmed exploitation — proving actual bypass requires a live request against the
         real API, which this skill does not perform.
      5. **Verdict** — approve / approve-with-notes / block.
      6. **Open questions or out-of-scope items** — e.g., "confirming the API layer actually rejects
         unauthorized requests requires a live request or reading server-side code outside this
         diff's scope," or "server SPA-fallback configuration is outside the reviewed files; flag for
         a separate infra review."
      
      ## When to push back
      
      Push back if the user asks to:
      
      - approve a route as "protected" solely because a `beforeEach`/`beforeEnter` guard redirects
        unauthenticated users, with no evidence the underlying API/data endpoint independently
        enforces authorization — a client-side redirect is not access control,
      - treat a redirect-target validation as done because "we checked it's a string" or "we blocked
        `http://`" — a type check or substring blocklist is not an allowlist and does not clear an
        open-redirect finding,
      - approve a dynamic link binding because "Vue Router handles URLs safely" — Vue Router does not
        validate URL schemes; that is application-code responsibility,
      - skip tracing a `route.params`/`route.query` value into `v-html` because "it's just the search
        box echoing what the user typed" — a reflected payload via a shared URL is exploitable
        immediately, with no stored/second-order step required,
      - downgrade a missing self-exclusion check in a redirect guard to informational because "it
        hasn't looped in testing" — Vue Router's own docs treat this check as required, not optional,
        and the loop condition depends on navigation order that testing may not have exercised.
      
  • metadata.json 2.3 KB
    {
      "id": "vue-router-navigation-security-review",
      "name": "Vue Router Navigation Security Review",
      "type": "skill",
      "provider": "frontend",
      "harnesses": [
        "claude-code",
        "cursor",
        "codex",
        "gemini",
        "kiro",
        "other"
      ],
      "summary": "Statically reviews Vue Router navigation guards, redirect flows, dynamic link bindings, and route configuration for client-side guards used as the sole authorization boundary, open redirects via route.query.redirect/returnUrl, javascript:/data: scheme injection through dynamic :to/:href bindings, reflected XSS from route params/query reaching v-html/innerHTML, guard-induced redirect loops, and catch-all/history-mode misconfiguration, grounding claims via Context7 against Vue Router's own documentation.",
      "source_type": "original",
      "official_docs": [
        "https://router.vuejs.org/guide/advanced/navigation-guards.html",
        "https://router.vuejs.org/guide/essentials/redirect-and-alias.html",
        "https://router.vuejs.org/guide/essentials/dynamic-matching.html",
        "https://router.vuejs.org/guide/essentials/history-mode.html",
        "https://vuejs.org/guide/best-practices/security.html",
        "https://owasp.org/www-community/attacks/xss/",
        "https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html"
      ],
      "security_notes": "This skill's entire scope is security-critical: a client-side-only navigation guard treated as an authorization boundary is a broken access control defect, an unvalidated route.query.redirect/returnUrl is an open-redirect vector, an unvalidated dynamic :to/:href is a javascript:/data:-scheme injection vector, and route params/query reaching v-html/innerHTML is reflected XSS. Every finding in this skill defaults to HIGH severity unless proven otherwise with concrete evidence (a confirmed server-side check, a same-origin allowlist, a protocol allowlist, or a sanitizer call on the exact traced path). Static-review-only skill: it reads and greps router configuration, guards, and template bindings but never executes, builds, or runs application code, and never sends live requests.",
      "last_verified": "2026-07-03",
      "path": "skills/frontend/vue-router-navigation-security-review",
      "author": "github: VincentChuWaiChow",
      "version": "0.1.0"
    }
    
  • SKILL.md 10.1 KB
    ---
    name: vue-router-navigation-security-review
    description: Statically review Vue Router navigation guards, redirect flows, and template bindings for client-side guards used as the sole authorization boundary, open redirects via route.query.redirect/returnUrl, javascript:/data: scheme injection through dynamic :to/:href bindings, route params/query reaching v-html/innerHTML (reflected XSS), guard-induced redirect loops, and catch-all/history-mode misconfiguration, grounded in Vue Router's own documentation.
    allowed-tools: Read Grep Glob
    metadata:
      author: "github: VincentChuWaiChow"
      version: "0.1.0"
      updated: "2026-07-03"
      category: security
    ---
    
    # Vue Router Navigation Security Review
    
    ## Purpose
    
    Review Vue Router navigation guards, redirect flows, dynamic link bindings, and route
    configuration for the security-critical defect classes specific to *client-side routing as a
    security surface* — not general route-tree architecture, not component design, and not the
    state-store or SSR-hydration concerns owned by sibling skills. This skill exists to keep the
    review anchored to five documented defect classes: (a) a `beforeEach`/`beforeEnter` guard used
    as the sole authorization boundary with no server-side enforcement behind it, (b) open redirect
    via `route.query.redirect`/`returnUrl` passed unvalidated into `router.push()`/
    `window.location`, (c) `javascript:`/`data:` scheme injection via a dynamic `:to`/`:href`
    binding, (d) `route.params`/`route.query` interpolated into `v-html`/`innerHTML` (reflected XSS
    through the router), and (e) `next(unvalidatedPath)`/guard-returned redirects creating loops or
    bypasses, overly-broad catch-all routes, and `history`-mode server misconfiguration. It does not
    re-litigate general route-tree/code-splitting architecture, Pinia/Vuex store security, or SSR
    cross-request pollution in every response.
    
    ## When to use
    
    Use this skill when the user asks to:
    
    - review a `router.beforeEach`/`beforeEnter` guard (or a Nuxt `definePageMeta`/route-middleware
      equivalent) that gates access to a route,
    - audit a login/logout flow's post-authentication redirect handling,
    - assess whether a `<router-link :to="...">` or dynamic `:href` binding is safe from scheme
      injection,
    - investigate whether `route.params`/`route.query` reaching a `v-html` or `.innerHTML` sink is
      exploitable,
    - review a route table for redirect-loop risk, catch-all overreach, or `history`-mode
      server-fallback exposure,
    - perform a pre-launch security review of a Vue Router-based application's navigation layer.
    
    Do not use this skill for:
    
    - general route-tree structure, loader/code-splitting, or focus-management review with no
      security angle — use `routing-navigation-review` instead (and note it targets React
      Router/Next.js, not Vue Router),
    - Pinia/Vuex store security (persisted sensitive data, SSR store-singleton pollution, untrusted
      hydration payloads, client-held role flags as an authorization source) — use
      `vue-state-store-security-review` instead,
    - SSR entry-point cross-request state pollution or general `v-html`/dynamic-URL injection
      unrelated to router data — use `vue-ssr-security-review` instead; this skill's injection scope
      is specifically `route.params`/`route.query` reaching a sink, not every `v-html` in the app,
    - broad token-storage, cookie-flag, CSRF, or OAuth/OIDC flow review with no router-specific
      mechanism in play — use `frontend-auth-session-security-review` instead; this skill covers
      only the router-mediated open-redirect mechanism (`route.query.redirect`/`returnUrl`), not
      session/token architecture generally,
    - confirming a bypass has already been exploited in production (concurrent-request reproduction,
      live penetration testing, a captured attack) — static analysis proves the structural risk, not
      that it has already been exploited.
    
    ## Context7 Documentation Protocol
    
    - Resolve the Vue Router library ID with `resolve-library-id` (matched result: `/vuejs/router`,
      Vue Router's own source-and-docs repository) before citing any guard-mechanism, redirect-API,
      or dynamic-matching claim.
    - Use `query-docs` against `/vuejs/router` to corroborate: navigation-guard return semantics
      (`beforeEach`/`beforeEnter` returning `false`/a location/`undefined`, and the legacy `next`
      argument's call-exactly-once contract), the documented `to.name !== 'Login'` self-exclusion
      pattern, redirect function shapes (`redirect: to => ({ path, query })`), `route.params`/
      `route.query` exposure with no built-in escaping, `router.push`'s acceptance of bare string
      paths, `createWebHistory()`'s server-fallback requirement, and the `/:pathMatch(.*)*`
      catch-all pattern. These are all `repo evidence` claims — cite the specific guide file
      (navigation-guards, redirect-and-alias, dynamic-matching, history-mode) alongside the claim.
    - The conclusion that a client-side guard must not be the *sole* authorization boundary is not
      itself a Vue-Router API fact — Vue Router's docs describe guards strictly as navigation
      control flow (cancel/redirect/proceed), never as an access-control mechanism. Label this
      specific conclusion `inference`, grounded in general server-side-enforcement security practice
      (`documentation-based` via OWASP), layered on the `repo evidence` fact that guards are
      browser-executed callbacks.
    - The claim that `v-html` bypasses Vue's default template auto-escaping is general Vue
      template-compiler behavior, not a Vue-Router API — ground it in the Vue security guide
      (`documentation-based`, listed in this skill's `official_docs`), not in `/vuejs/router`.
    - Read `package.json` first to confirm the Vue Router major version in use — guard signatures
      (return-value guards vs. the legacy `next` third argument), redirect object shape, and
      catch-all syntax (`/:pathMatch(.*)*` in v4 vs. legacy `*` in v3) differ across majors; do not
      apply v4 API names to a v3 codebase or vice versa.
    - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's
      `metadata.json` and label the claim `documentation-based, unverified against current release`.
    
    ## Lean operating rules
    
    - Every finding in this skill's scope defaults to HIGH severity: broken client-side-only access
      control, open redirect, scheme injection, and reflected XSS are all directly exploitable
      without further chaining. Do not downgrade to MEDIUM because "it hasn't been reported
      exploited yet" — the risk is structural.
    - Trace every finding to a concrete file:line and a concrete data-flow path (guard → endpoint
      gap, or origin → sink hops for injection/redirect findings). A finding that says "this guard
      might not be enough" or "this redirect might be exploitable" without the specific trace is a
      guess, not a finding.
    - Never flag "a `beforeEach`/`beforeEnter` guard exists" by itself. The finding requires the
      *absence* of confirmed server-side enforcement on the endpoint the protected route depends on
      — look for evidence (a 401/403 check, a BFF layer, an API contract note) before concluding
      it's missing, and state explicitly what you found or didn't find.
    - Never accept a type check or substring blocklist as clearing an open-redirect or
      scheme-injection finding. The only acceptable control is an allowlist: same-origin/
      relative-path validation for redirects, and an explicit protocol allowlist
      (`http:`/`https:`/`mailto:`) for dynamic link bindings.
    - Do not approve a `v-html`/`.innerHTML` sink fed by `route.params`/`route.query` unless a named
      sanitizer call is visibly present on that exact traced path — a sanitizer existing elsewhere
      in the codebase does not clear this bar.
    - Check every unconditional redirect-on-guard for the framework's own documented self-exclusion
      pattern (`to.name !== 'Login'` or equivalent). Its absence is a HIGH redirect-loop finding, not
      a style nit — Vue Router's docs treat it as required.
    - Read the full `routes` array in registration order before clearing a catch-all route; a
      broad/catch-all pattern registered before a route that should require auth can shadow it.
    - 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 full decision tree across all five defect classes, and the
      required output shape.
    - [Client-side guards as an authorization boundary, and navigation control-flow risks](references/client-guards-and-control-flow.md)
      — load when reviewing a `beforeEach`/`beforeEnter` guard, a guard-returned redirect, a
      catch-all route, or `history`-mode configuration.
    - [Open redirect, scheme injection, and reflected XSS through the router](references/open-redirect-and-injection.md)
      — load when reviewing a post-login redirect flow, a dynamic `:to`/`:href` binding, or a
      `v-html`/`innerHTML` sink fed by `route.params`/`route.query`.
    
    ## Response minimum
    
    Return, at minimum:
    
    - the guard(s), redirect flow(s), dynamic link binding(s), route table, and/or `v-html`/
      `innerHTML` sink(s) in scope,
    - ranked findings with file:line evidence, defect category
      (`client-guard-as-sole-authz` / `open-redirect` / `scheme-injection` / `reflected-xss` /
      `redirect-loop` / `catch-all-misconfig` / `history-mode-server-misconfig`), the concrete
      data-flow trace (the guard-to-endpoint gap, or the full origin-to-sink path naming every hop),
      and a fix sketch matching Vue Router's documented pattern,
    - for every guard finding, an explicit statement of whether server-side enforcement evidence was
      found and where — never infer it exists without evidence on the traced path,
    - evidence level per finding (`repo evidence`, `documentation-based`, or `inference`), with
      structural-risk findings explicitly labeled as structural risk, not confirmed-exploited,
    - verdict (approve / approve-with-notes / block),
    - open questions or scope the review could not cover (e.g., "confirming the API layer actually
      rejects unauthorized requests requires a live request or reading server-side code outside this
      diff's scope").
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related