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
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/vue-router-navigation-security-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Vue 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/beforeEnterguard (or a NuxtdefinePageMeta/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:hrefbinding is safe from scheme injection, - investigate whether
route.params/route.queryreaching av-htmlor.innerHTMLsink 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-reviewinstead (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-reviewinstead, - SSR entry-point cross-request state pollution or general
v-html/dynamic-URL injection unrelated to router data — usevue-ssr-security-reviewinstead; this skill's injection scope is specificallyroute.params/route.queryreaching a sink, not everyv-htmlin 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-reviewinstead; 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-docsagainst/vuejs/routerto corroborate: navigation-guard return semantics (beforeEach/beforeEnterreturningfalse/a location/undefined, and the legacynextargument's call-exactly-once contract), the documentedto.name !== 'Login'self-exclusion pattern, redirect function shapes (redirect: to => ({ path, query })),route.params/route.queryexposure 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 allrepo evidenceclaims — 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-basedvia OWASP), layered on therepo evidencefact that guards are browser-executed callbacks. - The claim that
v-htmlbypasses 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'sofficial_docs), not in/vuejs/router. - Read
package.jsonfirst to confirm the Vue Router major version in use — guard signatures (return-value guards vs. the legacynextthird 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_docsURLs in this skill'smetadata.jsonand label the claimdocumentation-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/beforeEnterguard 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/.innerHTMLsink fed byroute.params/route.queryunless 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
routesarray 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 — 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
— load when reviewing a
beforeEach/beforeEnterguard, a guard-returned redirect, a catch-all route, orhistory-mode configuration. - Open redirect, scheme injection, and reflected XSS through the router
— load when reviewing a post-login redirect flow, a dynamic
:to/:hrefbinding, or av-html/innerHTMLsink fed byroute.params/route.query.
Response minimum
Return, at minimum:
- the guard(s), redirect flow(s), dynamic link binding(s), route table, and/or
v-html/innerHTMLsink(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, orinference), 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.
Reviews (0)
No reviews yet.
No comments yet.