react-rsc-data-boundary-review
Statically review React Server Components code for data leaks across the server-to-client serialization boundary — secrets passed as props to Client Components, server-only modules missing the `server-only` guard, `use server` actions with no authorization check, non-public envir
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/react-rsc-data-boundary-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
React RSC Data Boundary Review
Purpose
Review React Server Components (RSC) code for the concrete, documented ways sensitive server-side data leaks across the server-to-client serialization boundary: a secret passed as a prop such as password={SECRET_API_KEY} on a Client Component, a server-only data module with no server-only guard that a client bundle could accidentally pull in, a 'use server' Server Action that mutates data with no session/ownership check (missing the session?.user check pattern), a 'use client' module reading a non-NEXT_PUBLIC_ environment variable, or a value that should have been marked with experimental_taintUniqueValue and narrowed before crossing the boundary. This skill exists so the review stays anchored to these five documented defect classes instead of drifting into a general "React code review" of component design, hooks usage, or rendering performance.
When to use
Use this skill when the user asks to:
- review a Server Component that passes props to a Client Component,
- audit a data-access module (
lib/,data/, or similar) that is meant to run only on the server, - review a
'use server'Server Action or Server Function for authorization gaps, - check whether a
'use client'module is reading environment variables safely, - perform a pre-launch security review of a Next.js App Router or other RSC-based application's data flow.
Do not use this skill for:
- generic client-side XSS review (
dangerouslySetInnerHTML, unsanitizedinnerHTML, DOM-based injection) — that is a distinct defect class covered byfrontend-dom-xss-csp-review, not this skill, - Next.js rendering/caching-strategy review (revalidation, cache tags,
fetchcache options) with no data-boundary security angle — usenextjs-rendering-caching-reviewornextjs-app-router-data-fetching-reviewinstead, - confirming that a leak has actually been exploited in production — static analysis proves the structural risk (a secret is reachable across the boundary) and not that an attacker has already captured it; that requires live request/network interception, which this skill does not perform.
Context7 Documentation Protocol
- Resolve the React library ID with
resolve-library-id(matched result:/reactjs/react.dev) before citing anyexperimental_taintUniqueValueor Server/Client Component serialization claim. Usequery-docsagainst it to confirm the exact tainting API shape and the documented anti-pattern (a secret such asprocess.env.API_PASSWORDpassed directly as a prop) before flagging a finding asdocumentation-based. - Resolve the Next.js library ID with
resolve-library-id(matched result:/vercel/next.js) before citing anyserver-onlypackage usage,NEXT_PUBLIC_environment-variable convention, or Server Action authorization pattern claim. Usequery-docsagainst it for theserver-onlyinstall/import pattern and the documented Server Action authorization example (auth()+ ownership check) before labeling a findingdocumentation-based. experimental_taintUniqueValueis an experimental React API, not yet stable — label any finding or fix sketch that depends on it asdocumentation-based (experimental API), and note that the equally valid non-experimental fix is simply omitting the sensitive field from the object passed to the Client Component.- Confirm which surface is in scope before citing a documented rule: a "Server Component passing props" claim only applies where a Server Component (no
'use client'directive, or an RSC-context module) actually renders a Client Component; a'use server'claim only applies to a function or file carrying that exact directive; aNEXT_PUBLIC_claim is Next.js–specific and does not automatically apply to a different meta-framework's env-var convention. - 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
- All five defect categories default to HIGH severity. This is a security-scoped skill: do not downgrade an untraced secret-prop pass, a missing
server-onlyguard, or an unauthorizeduse servermutation to MEDIUM just because it has not been observed exploited yet — the risk is in the structure, not in whether someone has already hit it. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this prop might leak a secret" without naming the specific prop, the specific Server Component that renders it, and the specific Client Component receiving it is not a valid finding — it is a guess.
- Do not flag every prop passed from a Server Component to a Client Component. Only a prop whose value traces back to an environment variable, a credential, a token, a full unfiltered config/response object, or any other server-only sensitive value is a finding. A plain string, number, or narrowed non-sensitive field (e.g.
config.SERVICE_API_VERSION) is not a finding. - Do not approve a
'use server'action that mutates or deletes data unless a session check (thesession?.userpattern) and, where the mutation targets a specific resource, an ownership check comparing the resource's owner to the authenticated user, are both visibly present on that exact function's path. A session check existing in a different action does not clear this bar — trace the specific function under review. - Do not treat a
server-onlyimport anywhere in the codebase as covering every server-only module. Confirm the exact file that reads the sensitive value (process.env.*, a database credential, an internal API token) hasserver-onlyimported at its own top, not merely that some other file in the project has it. - Watch for whole-object prop forwarding: a Server Component that fetches a config/response object and forwards it unnarrowed as a same-named prop (e.g.
config={config}) crosses the boundary with whatever sensitive fields the object holds, even if no single field is individually named "secret" or "password" in the forwarding code. Narrowing to specific non-sensitive fields, or applyingexperimental_taintUniqueValueto the sensitive fields before any possible pass-through, are the two documented mitigations. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only).
- Load only the reference needed for the concern in scope.
References
Load these only when needed:
- Review workflow and findings contract — use for the step-by-step review procedure, the decision tree across all five defect categories, and the required output shape.
- Boundary data leaks and the taint API — load only when the review scope includes a prop passed from a Server Component to a Client Component, a
server-onlyguard question, or a value that should have been tainted. - Server Action authorization and client environment exposure — load only when the review scope includes a
'use server'directive or a'use client'module readingprocess.env.
Response minimum
Return, at minimum:
- the Server Component(s), Client Component prop boundaries,
'use server'action(s), and/or'use client'module(s) in scope, - ranked findings with file:line evidence, defect category (
boundary-data-leak,missing-server-only-guard,action-authz-missing,env-exposure-in-client, ortaint-boundary-violation), the concrete data-flow trace (the sensitive value's origin and every hop to the boundary crossing or missing guard), and a fix sketch matching React's or Next.js's documented pattern, - guard status per finding: an explicit statement of whether a
server-onlyimport, anexperimental_taintUniqueValuecall, or a session/ownership check is present on the traced path — never approve on the assumption one exists elsewhere, - evidence level per finding (
repo evidence,documentation-based, orstructural-risk), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block),
- open questions or scope the review could not cover (e.g., "confirming data leakage requires live request interception (network inspection), not static review" or "taint API coverage requires a Context7 audit of all async boundaries where promises serialize, beyond this review's scope").
Files (vanguard-frontier-agentic)
-
references
-
boundary-data-leaks-and-taint.md 7.4 KB
# Boundary Data Leaks and the Taint API Use this reference only when the review scope includes a prop passed from a Server Component to a Client Component, a `server-only` guard question, or a value that should have been tainted before crossing the boundary. ## What people get wrong The naive assumption is: > "It's a Server Component doing the fetching, so anything it computes and hands down as props is automatically safe." Wrong. A Server Component running on the server can read anything the server process can reach — environment variables, database credentials, internal API tokens — but the moment any of that value is passed as a prop to a Client Component, React serializes it into the payload the browser receives. "Computed on the server" is not the same as "safe to send to the client." The recurring real failure mode is not a developer typing `password={rawSecret}` on purpose; it is a Server Component fetching a config or session object for its own server-side use, then forwarding the *entire* object unnarrowed to a Client Component because the object also happens to contain a few fields the client legitimately needs. ## Officially grounded rules React's own reference documentation states the anti-pattern directly, using exactly this shape: ```js export async function Dashboard(props) { // DO NOT DO THIS return <Overview password={process.env.API_PASSWORD} />; } ``` (`documentation-based`, React reference: `experimental_taintUniqueValue`.) React's documented mitigation is `experimental_taintUniqueValue`, called on the server before the value can ever reach a serialization boundary: ```js import "server-only"; import { experimental_taintUniqueValue } from 'react'; experimental_taintUniqueValue( 'Do not pass the API token password to the client. ' + 'Instead do all fetches on the server.', process, process.env.API_PASSWORD ); ``` Once tainted, React throws if that exact value is later passed to a Client Component or a Server Function — turning an accidental leak into a hard error instead of a silent shipment to the browser. This API is experimental and only available inside Server Components (`documentation-based`, React reference). The equally valid, non-experimental mitigation — and the one to recommend when the codebase cannot yet depend on an experimental API — is narrowing: return only the specific non-sensitive fields a Client Component needs, never the full object that also carries the sensitive ones (e.g. return `config.SERVICE_API_VERSION`, not `config`). Next.js's own documentation independently reinforces the server/client environment-variable split this rests on: server-only values (e.g. `process.env.DATABASE_URL`) are read directly in Server Components, while values intended for client code must be exposed through the `NEXT_PUBLIC_` prefix convention — there is no other sanctioned path for a raw secret to reach client code (`documentation-based`, Next.js docs). ## Non-negotiable design rules ### 1. Trace every prop's value to its origin before judging it Do not evaluate `<ClientComponent someProp={value} />` in isolation. Is `value` a literal? A prop passed through unchanged from further up? A field read from `process.env`? A field on a database row or API response? The finding depends on where that trace terminates, not on the prop name alone — though a prop named `password`, `apiKey`, `secret`, or `token` bound directly to an env-derived value is close to a self-evident finding. ### 2. Whole-object forwarding is a distinct, easy-to-miss failure mode `<ClientComponent config={config} />`, where `config` is the full object returned by a server-side fetch, forwards every field the object contains — including any the developer never explicitly intended to expose. This does not require a developer to type a dangerous-looking field name; it only requires forgetting that the object holds more than what the Client Component actually renders. Always ask: does this Client Component receive the *exact* fields it needs, or the whole object those fields came from? ### 3. `server-only` guards the module, not the specific call site `import 'server-only'` at the top of a module causes a build-time error if any Client Component (or a file in the client bundle graph) ever imports that module — but it only protects the module it is declared in. A sibling data-access file with the same kind of `process.env` read and no `server-only` import of its own is not covered by a guard that exists elsewhere in the codebase. ### 4. An unused prop still serialized is still a leak If a Client Component receives a sensitive prop but never reads it in its render output, the value has still been serialized into the payload sent to the browser and is visible in dev tools, network inspection, or the page source. "It's not rendered anywhere" does not clear a `boundary-data-leak` finding. ### 5. Taint and narrowing are complementary, not alternatives to trace around Do not accept "this codebase uses `experimental_taintUniqueValue` elsewhere" as clearing a specific untraced prop pass. Confirm the taint call (or the narrowing) is on the exact path from the sensitive origin to this specific Client Component boundary. ## Minimal safe implementation pattern ```tsx // Safe: narrow to a specific non-sensitive field. import { getSystemConfig } from './config' // has `import 'server-only'` at its top export async function Dashboard() { const config = await getSystemConfig() return <ClientDashboard version={config.SERVICE_API_VERSION} /> } ``` Anti-pattern (do not approve): ```tsx export async function Dashboard(props) { // DO NOT DO THIS return <Overview password={process.env.API_PASSWORD} />; } ``` ```tsx // Also unsafe: whole object forwarded, even with no field literally named "password". export async function Dashboard() { const config = await getSystemConfig() return <ClientDashboard config={config} /> } ``` ## Adversarial checklist Before clearing a Server Component → Client Component prop boundary as safe: - What is the literal origin of every prop value — a literal, a narrowed field, or a full object/response? - If a full object is forwarded, does it contain any field that reads `process.env`, a credential, or a token anywhere upstream in its construction? - Is there a `server-only` import at the top of the specific module that reads the sensitive value, or only somewhere else in the codebase? - Is there an `experimental_taintUniqueValue` call on the exact path from the sensitive value's origin to this boundary, or is narrowing the only control in place? - Would a future code change (a new field added to the forwarded object, or a refactor that removes the narrowing) reach the client without re-triggering a security review? If any answer is unclear or reveals a gap, the finding is HIGH — do not soften it to "worth double-checking." ## Verification targets - Grep for JSX usages that pass a prop from a Server Component to an imported Client Component (a component whose file has `'use client'` at its top), and enumerate every prop value. - Grep for `password=`, `apiKey=`, `secret=`, `token=` (and case variants) bound to any non-literal expression in JSX. - Grep for a prop name identical to the bound variable name (`config={config}`-shaped whole-object forwarding). - Grep every data-access module for `process.env.` reads and confirm `import 'server-only'` is present as the first statement in that same file. - Grep for `experimental_taintUniqueValue(` calls and confirm which specific values they cover. -
server-actions-and-env-exposure.md 7 KB
# Server Action Authorization and Client Environment Exposure Use this reference only when the review scope includes a `'use server'` directive (a Server Action/Function) or a `'use client'` module reading `process.env`. ## What people get wrong The naive assumption for Server Actions is: > "It's a `'use server'` function, so it only runs on the server, and only my app's own UI calls it — that's authorization enough." Wrong. A `'use server'` directive controls *where the code executes*, not *who is allowed to invoke it*. Next.js compiles every Server Action into a callable server endpoint; anyone who can reach that endpoint — not just users going through the app's own rendered UI — can invoke it directly with an arbitrary argument. A Server Action with no session check is an unauthenticated mutation endpoint. A Server Action with a session check but no ownership check is an authenticated *Insecure Direct Object Reference* (IDOR): any logged-in user can pass any other user's resource ID. The naive assumption for client-side env access is: > "I need a config value in this Client Component, so I'll just read `process.env.SOMETHING` like I would on the server." Wrong. In a `'use client'` module, only environment variables the framework has explicitly exposed to the browser bundle are ever available at runtime; anything else is `undefined` once the code actually runs in the browser, no matter what it appeared to be when read in server-side tooling or type definitions. Beyond the functional bug, the read itself is a signal the developer's mental model of the server/client boundary is broken — a strong prompt to check the rest of that same module for other assumptions that might leak something real. ## Officially grounded rules Next.js's own documentation shows the required Server Action authorization pattern directly: ```tsx 'use server' import { auth } from '@/lib/auth' import { db } from '@/lib/db' export async function deletePost(postId: string) { const session = await auth() if (!session?.user) { throw new Error('Unauthorized') } const post = await db.post.findUnique({ where: { id: postId } }) // Check that the user owns this resource if (post.authorId !== session.user.id) { throw new Error('Forbidden') } await db.post.delete({ where: { id: postId } }) } ``` (`documentation-based`, Next.js docs: Server Action data-security guide.) Both checks are present: a session check (`session?.user`) proving the caller is authenticated, and an ownership check (`post.authorId !== session.user.id`) proving the caller may act on *this specific* resource — the session check alone does not imply the ownership check. Next.js's documentation is equally direct on client environment access: `'use client'` modules can read environment variables prefixed `NEXT_PUBLIC_`; anything else read server-side (e.g. `process.env.DATABASE_URL`) is a server-only value with no client-side equivalent (`documentation-based`, Next.js docs). ## Non-negotiable design rules ### 1. A session check is necessary but not sufficient for resource-scoped mutations For a `'use server'` function that takes an ID and mutates/deletes/reads the corresponding resource, require both: a session check (is anyone authenticated at all) and an ownership/authorization check (may *this* authenticated user act on *this specific* resource). A function that checks only the former and trusts the caller-supplied ID unconditionally is IDOR-shaped. ### 2. Do not accept "the UI never lets you delete someone else's post" as a control A Server Action is a callable endpoint independent of what the rendered UI happens to expose. The fact that the app's own components never construct a request for another user's resource ID says nothing about what a direct request to the compiled action endpoint can supply. ### 3. `NEXT_PUBLIC_` is the only sanctioned bridge for client-visible env values There is no other supported mechanism for exposing a build-time environment value to `'use client'` code short of an explicit runtime fetch to a server endpoint that itself performs its own authorization. A non-`NEXT_PUBLIC_` read in a `'use client'` file is either dead code that silently resolves to `undefined`, or evidence of a broken assumption worth investigating further in the same module. ### 4. Trace `'use server'` scope precisely A file-level `'use server'` directive applies to every export in that file; a function-level `'use server'` directive applies only to that function. Confirm which form is in use before asserting a specific exported function is a Server Action in scope for this review. ## Minimal safe implementation pattern ```tsx 'use server' import { auth } from '@/lib/auth' import { db } from '@/lib/db' export async function deletePost(postId: string) { const session = await auth() if (!session?.user) { throw new Error('Unauthorized') } const post = await db.post.findUnique({ where: { id: postId } }) if (post.authorId !== session.user.id) { throw new Error('Forbidden') } await db.post.delete({ where: { id: postId } }) } ``` Anti-pattern (do not approve): ```tsx 'use server' export async function deletePost(postId: string) { await db.post.delete({ where: { id: postId } }) } ``` Client environment access: ```tsx 'use client' // Safe: build-time-injected public variable. const publicEndpoint = process.env.NEXT_PUBLIC_API_URL ``` ```tsx 'use client' // Unsafe: undefined at runtime, and a signal of a broken server/client mental model. const apiKey = process.env.DATABASE_URL ``` ## Adversarial checklist Before clearing a `'use server'` function as safe: - Is there a session check (`session?.user` or the framework's equivalent) before any read/write/delete against the data layer? - If the function mutates or reads a specific resource by ID, is there a separate ownership/authorization check comparing the resource's owner to the authenticated user? - Could a caller invoke this function directly (bypassing the app's own rendered UI) with an arbitrary ID and reach the mutation regardless of what the UI would normally allow? Before clearing a `'use client'` module's environment access as safe: - Does every `process.env.*` read in this file use the `NEXT_PUBLIC_` prefix (or the framework's equivalent public convention)? - If not, is the read genuinely unreachable dead code, or does it suggest the developer believed a server-only value would be available here? If any answer reveals a gap, the finding is HIGH (`action-authz-missing`) or MEDIUM (`env-exposure-in-client`) — do not soften either to "worth double-checking." ## Verification targets - Grep for `'use server'` (file-level and function-level) and enumerate every exported function in scope. - For each, grep the function body for an `auth(`/`session` check and, separately, for an ownership comparison (`.authorId`, `.userId`, `.ownerId`, or equivalent) against the authenticated session before any `db.*.delete(`/`db.*.update(`/mutating call. - Grep for `'use client'` and, within each such file, grep for `process.env.` reads; flag every one not prefixed `NEXT_PUBLIC_`. -
workflow-and-output.md 7 KB
# Review Workflow and Findings Contract Use this reference for the step-by-step review procedure and the required output shape. Load the other two references only for the specific defect class the RSC code under review actually raises. ## Prerequisites - Identify the RSC boundary in scope: which files are Server Components (no `'use client'` directive, or files in an RSC-only context such as a Next.js App Router `app/` tree without the directive), which are Client Components (`'use client'` at the top), which files carry a `'use server'` directive (Server Actions/Functions), and which data-access modules are expected to be server-only. - Read `package.json` to confirm the framework and version in use (Next.js App Router, a different RSC-capable meta-framework, or plain `react-server-dom-*` wiring) — API names and exact conventions (e.g. `NEXT_PUBLIC_` is Next.js-specific) differ by framework; do not assume a Next.js convention applies to a non-Next.js RSC setup. ## Workflow 1. **Locate every Server Component → Client Component prop boundary.** For each Server Component that renders a Client Component, list every prop passed and trace each value's origin (a literal, an env var, a database/API response field, a full object). 2. **Enumerate server-only data-access modules.** For each module that reads `process.env`, a database credential, or an internal API token, check whether `import 'server-only'` is the first statement in that file. See `references/boundary-data-leaks-and-taint.md` for the decision tree. 3. **Check for tainting or narrowing on sensitive values.** For any Server Component that reads a config/response object containing sensitive fields before rendering a Client Component, determine whether the object is narrowed to only non-sensitive fields before being passed as a prop, or whether `experimental_taintUniqueValue` is applied to the sensitive fields upstream. 4. **Enumerate every `'use server'` function.** For each, check whether a session check (`auth()` or equivalent, tested via a `session?.user` style guard) and — for mutations targeting a specific resource — an ownership check comparing the resource owner to the authenticated user, both execute before any read/write/delete against the data layer. See `references/server-actions-and-env-exposure.md`. 5. **Enumerate every `'use client'` module that reads `process.env`.** For each environment variable read, confirm the name is prefixed `NEXT_PUBLIC_` (or the framework's equivalent public-env convention); flag any that is not. 6. **Produce ranked findings** using the output contract below. ## Decision tree - A Server Component passes a prop to a Client Component whose value traces back to an environment variable, a database credential, an API token, or any other value your review classifies as server-only sensitive → **HIGH** finding, `boundary-data-leak`. Cite React's documented anti-pattern (`documentation-based`). - A Server Component passes an entire config/response object (not narrowed to specific non-sensitive fields) to a Client Component, and that object is known or suspected to carry a sensitive field → **HIGH** finding, `taint-boundary-violation`. Note whether `experimental_taintUniqueValue` was available and unused, or whether simple narrowing was the missed opportunity. - A prop's value is a plain literal, a non-sensitive string/number, or a field explicitly narrowed out of a larger object (e.g. `config.SERVICE_API_VERSION`) → not a finding. - A data-access module reads `process.env`/a credential and has no `import 'server-only'` as its first statement → **HIGH** finding, `missing-server-only-guard`. This holds even if no Client Component currently imports the module — the guard is the structural control against a future accidental import, and its absence is the finding regardless of present-day reachability. - A `'use server'` function performs a read/write/delete against the data layer with no session check visible in its body → **HIGH** finding, `action-authz-missing`. - A `'use server'` function has a session check but, for a mutation scoped to a specific resource (delete/update by ID), has no ownership check comparing the resource's owner to the authenticated user → **HIGH** finding, `action-authz-missing` (IDOR-shaped gap) — a session check alone proves *someone* is logged in, not that they own the resource being mutated. - A `'use client'` module reads a `process.env` variable not prefixed `NEXT_PUBLIC_` → **MEDIUM** finding, `env-exposure-in-client` (the value is `undefined` at runtime in the browser bundle, and the read itself signals a broken mental model of the boundary that may mask a worse leak elsewhere). - A `'use client'` module reads only `NEXT_PUBLIC_`-prefixed variables → not a finding. ## Output contract Every response from this skill must return: 1. **Scope** — the Server Component(s), Client Component prop boundaries, `'use server'` action(s), and/or `'use client'` module(s) reviewed. 2. **Ranked findings** — each with file:line, defect category (`boundary-data-leak` / `missing-server-only-guard` / `action-authz-missing` / `env-exposure-in-client` / `taint-boundary-violation`), the concrete data-flow trace (naming every hop from the sensitive value's origin to the boundary crossing or missing guard), and a fix sketch matching React's or Next.js's documented pattern. 3. **Guard status per finding** — an explicit statement of whether `server-only`, `experimental_taintUniqueValue`, or a session/ownership check is present on the traced path; never infer one exists elsewhere in the codebase. 4. **Evidence level per finding** — `repo evidence`, `documentation-based`, or `structural-risk`. Label structural risk findings as structural risk explicitly — do not imply confirmed exploitation without live evidence (e.g., a captured network payload showing the secret in the client bundle). 5. **Verdict** — approve / approve-with-notes / block. 6. **Open questions or out-of-scope items** — e.g., "confirming the secret actually reached a browser requires live request/network interception, not static review," or "this file also has a client-side XSS-shaped concern unrelated to the RSC boundary — out of scope for this skill, recommend `frontend-dom-xss-csp-review`." ## When to push back Push back if the user asks to: - approve a prop pass because "the Client Component doesn't actually use that field" — an unused sensitive prop still serializes into the client payload; unused-in-render is not the same as never-transmitted, - treat a `server-only` import anywhere in the project as covering a different file that itself reads the sensitive value with no guard of its own, - skip the ownership check on a `'use server'` mutation because "the session check already runs" — a session check proves authentication, not authorization over the specific resource being mutated, - downgrade an untraced whole-object prop pass to informational because "the object probably doesn't have anything sensitive in it" — trace the object's actual shape before clearing it, or state explicitly that the shape could not be confirmed.
-
-
metadata.json 1.9 KB
{ "id": "react-rsc-data-boundary-review", "name": "React RSC Data Boundary Review", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Reviews React Server Components code for data leaks across the server-to-client serialization boundary: secrets passed as props to Client Components, server-only modules missing the server-only guard, use server actions with no authorization check, non-public environment variables read in use client modules, and tainted values crossing the boundary unnarrowed, grounding claims via Context7 and React's and Next.js's own documentation.", "source_type": "original", "official_docs": [ "https://react.dev/reference/rsc/server-components", "https://react.dev/reference/react/experimental_taintUniqueValue", "https://nextjs.org/docs/app/getting-started/server-and-client-components", "https://nextjs.org/docs/app/guides/data-security" ], "security_notes": "This skill's entire scope is security-critical: a secret crossing the server-to-client serialization boundary is a data-exposure defect (potential credential/token leakage to every browser rendering the page), a missing server-only guard risks accidental client-bundle inclusion of secret-reading code, an unauthorized use server action is a mutation/IDOR vector, and non-public env exposure in a use client module signals a broken trust boundary. Every finding in this skill defaults to HIGH severity unless proven otherwise with concrete guard/narrowing evidence. Static-review-only skill: it reads and greps Server/Client Component source and server actions but never executes, builds, or runs application code, and never sends live requests.", "last_verified": "2026-07-03", "path": "skills/frontend/react-rsc-data-boundary-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 9 KB
--- name: react-rsc-data-boundary-review description: Statically review React Server Components code for data leaks across the server-to-client serialization boundary — secrets passed as props to Client Components, server-only modules missing the `server-only` guard, `use server` actions with no authorization check, non-public environment variables read in `use client` modules, and tainted values crossing the boundary unnarrowed — grounded in React's and Next.js's own documentation. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-03" category: security --- # React RSC Data Boundary Review ## Purpose Review React Server Components (RSC) code for the concrete, documented ways sensitive server-side data leaks across the server-to-client serialization boundary: a secret passed as a prop such as `password={SECRET_API_KEY}` on a Client Component, a server-only data module with no `server-only` guard that a client bundle could accidentally pull in, a `'use server'` Server Action that mutates data with no session/ownership check (missing the `session?.user` check pattern), a `'use client'` module reading a non-`NEXT_PUBLIC_` environment variable, or a value that should have been marked with `experimental_taintUniqueValue` and narrowed before crossing the boundary. This skill exists so the review stays anchored to these five documented defect classes instead of drifting into a general "React code review" of component design, hooks usage, or rendering performance. ## When to use Use this skill when the user asks to: - review a Server Component that passes props to a Client Component, - audit a data-access module (`lib/`, `data/`, or similar) that is meant to run only on the server, - review a `'use server'` Server Action or Server Function for authorization gaps, - check whether a `'use client'` module is reading environment variables safely, - perform a pre-launch security review of a Next.js App Router or other RSC-based application's data flow. Do not use this skill for: - generic client-side XSS review (`dangerouslySetInnerHTML`, unsanitized `innerHTML`, DOM-based injection) — that is a distinct defect class covered by `frontend-dom-xss-csp-review`, not this skill, - Next.js rendering/caching-strategy review (revalidation, cache tags, `fetch` cache options) with no data-boundary security angle — use `nextjs-rendering-caching-review` or `nextjs-app-router-data-fetching-review` instead, - confirming that a leak has actually been exploited in production — static analysis proves the structural risk (a secret is reachable across the boundary) and not that an attacker has already captured it; that requires live request/network interception, which this skill does not perform. ## Context7 Documentation Protocol - Resolve the React library ID with `resolve-library-id` (matched result: `/reactjs/react.dev`) before citing any `experimental_taintUniqueValue` or Server/Client Component serialization claim. Use `query-docs` against it to confirm the exact tainting API shape and the documented anti-pattern (a secret such as `process.env.API_PASSWORD` passed directly as a prop) before flagging a finding as `documentation-based`. - Resolve the Next.js library ID with `resolve-library-id` (matched result: `/vercel/next.js`) before citing any `server-only` package usage, `NEXT_PUBLIC_` environment-variable convention, or Server Action authorization pattern claim. Use `query-docs` against it for the `server-only` install/import pattern and the documented Server Action authorization example (`auth()` + ownership check) before labeling a finding `documentation-based`. - `experimental_taintUniqueValue` is an experimental React API, not yet stable — label any finding or fix sketch that depends on it as `documentation-based (experimental API)`, and note that the equally valid non-experimental fix is simply omitting the sensitive field from the object passed to the Client Component. - Confirm which surface is in scope before citing a documented rule: a "Server Component passing props" claim only applies where a Server Component (no `'use client'` directive, or an RSC-context module) actually renders a Client Component; a `'use server'` claim only applies to a function or file carrying that exact directive; a `NEXT_PUBLIC_` claim is Next.js–specific and does not automatically apply to a different meta-framework's env-var convention. - 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 - All five defect categories default to HIGH severity. This is a security-scoped skill: do not downgrade an untraced secret-prop pass, a missing `server-only` guard, or an unauthorized `use server` mutation to MEDIUM just because it has not been observed exploited yet — the risk is in the structure, not in whether someone has already hit it. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this prop might leak a secret" without naming the specific prop, the specific Server Component that renders it, and the specific Client Component receiving it is not a valid finding — it is a guess. - Do not flag every prop passed from a Server Component to a Client Component. Only a prop whose value traces back to an environment variable, a credential, a token, a full unfiltered config/response object, or any other server-only sensitive value is a finding. A plain string, number, or narrowed non-sensitive field (e.g. `config.SERVICE_API_VERSION`) is not a finding. - Do not approve a `'use server'` action that mutates or deletes data unless a session check (the `session?.user` pattern) and, where the mutation targets a specific resource, an ownership check comparing the resource's owner to the authenticated user, are both visibly present on that exact function's path. A session check existing in a different action does not clear this bar — trace the specific function under review. - Do not treat a `server-only` import anywhere in the codebase as covering every server-only module. Confirm the exact file that reads the sensitive value (`process.env.*`, a database credential, an internal API token) has `server-only` imported at its own top, not merely that some other file in the project has it. - Watch for whole-object prop forwarding: a Server Component that fetches a config/response object and forwards it unnarrowed as a same-named prop (e.g. `config={config}`) crosses the boundary with whatever sensitive fields the object holds, even if no single field is individually named "secret" or "password" in the forwarding code. Narrowing to specific non-sensitive fields, or applying `experimental_taintUniqueValue` to the sensitive fields before any possible pass-through, are the two documented mitigations. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only). - Load only the reference needed for the concern in scope. ## References Load these only when needed: - [Review workflow and findings contract](references/workflow-and-output.md) — use for the step-by-step review procedure, the decision tree across all five defect categories, and the required output shape. - [Boundary data leaks and the taint API](references/boundary-data-leaks-and-taint.md) — load only when the review scope includes a prop passed from a Server Component to a Client Component, a `server-only` guard question, or a value that should have been tainted. - [Server Action authorization and client environment exposure](references/server-actions-and-env-exposure.md) — load only when the review scope includes a `'use server'` directive or a `'use client'` module reading `process.env`. ## Response minimum Return, at minimum: - the Server Component(s), Client Component prop boundaries, `'use server'` action(s), and/or `'use client'` module(s) in scope, - ranked findings with file:line evidence, defect category (`boundary-data-leak`, `missing-server-only-guard`, `action-authz-missing`, `env-exposure-in-client`, or `taint-boundary-violation`), the concrete data-flow trace (the sensitive value's origin and every hop to the boundary crossing or missing guard), and a fix sketch matching React's or Next.js's documented pattern, - guard status per finding: an explicit statement of whether a `server-only` import, an `experimental_taintUniqueValue` call, or a session/ownership check is present on the traced path — never approve on the assumption one exists elsewhere, - evidence level per finding (`repo evidence`, `documentation-based`, or `structural-risk`), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block), - open questions or scope the review could not cover (e.g., "confirming data leakage requires live request interception (network inspection), not static review" or "taint API coverage requires a Context7 audit of all async boundaries where promises serialize, beyond this review's scope").
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.