Skip to content

feat: server function encryption#321

Merged
lazarv merged 5 commits intomainfrom
feat/server-action-encryption
Mar 1, 2026
Merged

feat: server function encryption#321
lazarv merged 5 commits intomainfrom
feat/server-action-encryption

Conversation

@lazarv
Copy link
Owner

@lazarv lazarv commented Mar 1, 2026

This PR adds AES-256-GCM encryption for server function IDs, preventing clients from discovering or tampering with internal module paths and export names that are normally embedded in the HTML payload as plain-text action references.

With this change, every server function reference sent to the client is replaced with an opaque, encrypted token. The server decrypts the token on invocation and maps it back to the original function. This is a zero-configuration feature — in development a random ephemeral key is generated automatically, and in production the key is persisted as a build artifact.

Motivation

Without encryption, server function IDs like src/actions#deleteUser are visible in the client bundle and HTML output. An attacker could:

  • Enumerate internal module paths and exported function names
  • Craft requests to invoke arbitrary server functions they shouldn't have access to
  • Gain insight into the server-side code structure

Encrypting these IDs eliminates this entire class of information disclosure.

What Changed

New: action-crypto.mjs — Core Encryption Engine

A new module (packages/react-server/server/action-crypto.mjs, ~310 lines) providing:

  • encryptActionId(id) — Encrypts a plain-text action ID using AES-256-GCM with a random 12-byte IV, producing a base64url token
  • decryptActionId(token) — Decrypts a token back to the original ID, trying the primary key first, then previous keys for rotation
  • initSecret(secret) / initSecretFromConfig(config) — Key initialization with a clear resolution order:
    1. REACT_SERVER_FUNCTIONS_SECRET env var
    2. REACT_SERVER_FUNCTIONS_SECRET_FILE env var (path to .pem)
    3. serverFunctions.secret in config
    4. serverFunctions.secretFile in config
    5. Build artifact (persisted secret)
    6. Random ephemeral key (dev fallback)
  • wrapServerReferenceMap(baseMap) — Proxy wrapper that transparently handles encrypted key lookups by decrypting tokens before delegating to the base map
  • Key rotation support via serverFunctions.previousSecrets and serverFunctions.previousSecretFiles config arrays

Modified: action-register.mjs — Encrypting $$id Getter

React's registerServerReference is wrapped to replace the plain-text $$id property with a lazily-encrypting getter. Key design decisions:

  • Cached per instance: The encrypted token is generated once (random IV) on first read and cached, ensuring $$id is stable across multiple reads of the same reference. This is critical because React's decodeFormState compares the $ACTION_ID from the submitted form with the action's current $$id.
  • $$originalId: The original plain-text ID is stored as a non-enumerable property for server-side identity comparison without decryption.

Modified: action-state.mjs — Dual-format actionId Comparison

useActionState now compares the context's actionId against both action.$$id (encrypted) and action.$$originalId (plain text), because the context can receive either format:

  • Form submissions (via decodeAction): actionId is the encrypted token from the form's $ACTION_ID hidden field
  • Programmatic calls (via React-Server-Action header): actionId is the decrypted plain-text ID after server-side resolution

Modified: render-rsc.jsx — Server-side Decryption

The RSC render function now:

  • Wraps the server reference map with wrapServerReferenceMap() at module level for transparent encrypted lookups
  • Decrypts the React-Server-Action header value before resolving the function
  • Falls back to the raw header value if decryption fails (plain-text IDs still work in dev)
  • Throws ServerFunctionNotFoundError when decryption fails and the raw ID is also unknown (tampered/invalid token)

Modified: use-server.mjs Plugin — Build-time Encryption for Client/SSR Stubs

The Vite plugin that transforms "use server" modules now encrypts action IDs at build time when generating createServerReference() calls for client and SSR bundles. This ensures the client never sees plain-text IDs.

Build & Runtime Integration

  • lib/build/action.mjs: Generates a fresh secret at build time, persists it as server/action-secret.mjs build artifact
  • lib/build/edge.mjs: Resolves the action-secret import alias for edge builds
  • lib/start/manifest.mjs: Loads the persisted secret from the build artifact at startup, then applies env/config overrides
  • lib/dev/action.mjs and lib/dev/index.mjs: Initialize the secret from config at dev server startup
  • Loader updates (node-loader.mjs, bun.mjs, deno.mjs): Add import alias for @lazarv/react-server/dist/server/action-secret pointing to the build output

Documentation

New docs page at docs/src/pages/en/(pages)/framework/server-function-encryption.mdx covering:

  • How encryption works (AES-256-GCM, per-reference tokens)
  • Zero-configuration defaults
  • Custom secret configuration (env vars, config, .pem files)
  • Key rotation with previousSecrets / previousSecretFiles
  • Error handling (ServerFunctionNotFoundError, error boundaries)
  • Security model and guarantees

Configuration

// react-server.config.mjs
export default {
  serverFunctions: {
    secret: "your-secret-key",
    // OR
    secretFile: "./secrets/action-key.pem",

    // For key rotation:
    previousSecrets: ["old-secret-1"],
    previousSecretFiles: ["./secrets/old-key.pem"],
  },
};

Environment variables:

REACT_SERVER_FUNCTIONS_SECRET=your-secret-key
# OR
REACT_SERVER_FUNCTIONS_SECRET_FILE=./secrets/action-key.pem

@cloudflare-workers-and-pages
Copy link

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
react-server-docs ebf2331 Mar 01 2026, 08:49 PM

@lazarv lazarv merged commit bf806bc into main Mar 1, 2026
143 of 145 checks passed
@lazarv lazarv deleted the feat/server-action-encryption branch March 1, 2026 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant