vitejs / vitejs/vite-plugin-react

[plugin-rsc] Dev server: unauthenticated invocation of arbitrary exports in 'use server' modules (no export-name validation in loadServerAction)

Open
#1,424 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

plugin: rsc
Dominant language
TypeScript
Stars
1.2k
Forks
269
Avg merge
1d 3h
Merged PRs (30d)
19

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, 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/plugin-rsc/src/core/rsc.ts and inspect loadServerAction, then read plugin.ts:357-405 and rsc/shared.ts:6-13 to trace dev-mode validation and module loading. Reproduce the issue with the starter example and the POST request described in the report. Done means unregistered exports can no longer be dispatched while registered server actions still work, with the reproduction rejected.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
backend, devtools, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.