Skip to content

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

Description

@emohack

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions