Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/src/pages/en/(pages)/features/micro-frontends.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,25 @@ To run the example, clone the `@lazarv/react-server` repository and run the foll
```sh
pnpm install
pnpm --filter ./examples/remote dev
```
```

<Link name="disabling-remote-components">
## Disabling Remote Components rendering
</Link>

Applications that don't host Remote Components can force-disable the entire rendering path by setting `remoteComponents: false` in the runtime config. The runtime then ignores the `@__react_server_remote__` URL marker, never enters the Remote Components code path, and never parses request bodies as Remote Components payloads.

```js
// react-server.config.mjs
export default {
remoteComponents: false,
};
```

Two effects worth understanding:

The remote-rendering routing is removed. URLs carrying the `@__react_server_remote__` marker are no longer treated as Remote Components fetches; they fall through to normal page rendering as if the marker weren't there.

Temporary references — the runtime mechanism that lets non-serializable client values (callbacks, DOM refs, opaque thenables) round-trip through `$T` tags on the wire — are gated by the same flag. Their only legitimate use is round-tripping values back to the same client during a Remote Components render, so when `remoteComponents: false` is set, the temporary-reference set is not created and any `$T` tag in an incoming request body is rejected as malformed. This eliminates a small but real attack surface: a fabricated body otherwise allocates proxy objects and runs opaque lookups against the in-memory reference map.

This pairs with `serverFunctions: false` (see [Disabling Server Functions entirely](../guide/server-functions#disabling-server-functions)) for apps that want to ship a strictly read-only surface — neither Server Functions nor Remote Components remain reachable, and any `POST`/`PUT`/`PATCH`/`DELETE` traffic flows through to normal rendering without parsing the request body.
105 changes: 61 additions & 44 deletions docs/src/pages/en/(pages)/guide/server-functions.mdx

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion docs/src/pages/ja/(pages)/features/micro-frontends.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,25 @@ export default {
```sh
pnpm install
pnpm --filter ./examples/remote dev
```
```

<Link name="disabling-remote-components">
## リモートコンポーネントのレンダリングを無効化する
</Link>

リモートコンポーネントをホストしないアプリケーションは、ランタイム設定に `remoteComponents: false` を指定することでレンダリングパス全体を強制的に無効化できます。これによりランタイムは `@__react_server_remote__` の URL マーカーを無視し、リモートコンポーネントのコードパスには入らず、リクエスト本体をリモートコンポーネントのペイロードとしてパースしません。

```js
// react-server.config.mjs
export default {
remoteComponents: false,
};
```

理解しておくべき効果は二つあります。

リモートレンダリングのルーティングが除去されます。`@__react_server_remote__` マーカーを含む URL はリモートコンポーネントへのフェッチとして扱われなくなり、マーカーがないかのように通常のページレンダリングへフォールスルーします。

ワイヤ上で `$T` タグを介して非シリアライザブルなクライアント値 (コールバック、DOM 参照、不透明な thenable) をラウンドトリップさせるランタイム機構である一時参照も、同じフラグでゲートされます。一時参照の正当な用途はリモートコンポーネントのレンダリング中に同じクライアントへ値を返送することのみであるため、`remoteComponents: false` が設定されている場合は一時参照セットを作成せず、受信リクエスト本体内のいかなる `$T` タグも不正として拒否します。これにより、攻撃者が偽造したリクエスト本体によってプロキシオブジェクトを確保させたり、メモリ上の参照マップに対する不透明な検索を実行させたりする、わずかではありますが実在する攻撃面を排除できます。

`serverFunctions: false` ([サーバ関数を完全に無効化する](../guide/server-functions#disabling-server-functions) を参照) と組み合わせることで、厳密に読み取り専用のサーフェスを公開するアプリケーションを構築できます。サーバ関数もリモートコンポーネントもアクセスできず、`POST` / `PUT` / `PATCH` / `DELETE` トラフィックはリクエスト本体をパースすることなく通常のレンダリングへフォールスルーします。
17 changes: 17 additions & 0 deletions docs/src/pages/ja/(pages)/guide/server-functions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,23 @@ export default {

バインド済みのサーバ関数がクライアントコンポーネントに渡され、クライアントがさらに `.bind(...)` を呼んで引数を追加した場合、これらの追加引数は新しいキャプチャではなく **ランタイム引数** として扱われます。これらは通常の呼び出し引数 (ユーザが呼び出し時に渡すものと同様) として送られ、暗号化されたトークンには含まれません。これは意図的な仕様です — サーバから発行されたバインドのみが整合性で保護され、クライアントが追加した引数は実質的にクライアントが呼び出し時に送ることを選んだ値に過ぎません。

<Link name="disabling-server-functions">
### サーバ関数を完全に無効化する
</Link>

アプリケーションがサーバ関数を一切持たない場合、ランタイム設定で `serverFunctions: false` を指定してください。これによりランタイムはアクションリクエストの復号を一切行わなくなります。受信した `POST` / `PUT` / `PATCH` / `DELETE` トラフィックはアクション呼び出しとしてパースされず、マニフェストも参照されず、リクエストは通常のページレンダリングへとフォールスルーします。アクションディスパッチを攻撃対象から取り除き、リクエストごとのわずかなオーバーヘッドも削減できます。

```js
// react-server.config.mjs
export default {
serverFunctions: false,
};
```

`"use server"` モジュールやインラインサーバ関数を一切含まない本番ビルドでは、ランタイムが空のマニフェストを自動検出し、明示的な設定なしで同じゲートを適用します。明示的な `false` は、ビルド前 (開発時) からゲートを効かせたい場合や、ビルド結果に依存しない明確な姿勢として無効化したい場合に使用します。

リモートコンポーネントのレンダリングも同じ多層防御の方針で `remoteComponents: false` により強制的に無効化できます。マイクロフロントエンドのページの [リモートコンポーネントのレンダリングを無効化する](../features/micro-frontends#disabling-remote-components) を参照してください。

<Link name="security-limitations">
### 制限事項
</Link>
Expand Down
44 changes: 28 additions & 16 deletions packages/react-server/config/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,25 +796,25 @@ export interface CacheConfig {
providers?: Record<string, unknown> | unknown[];
}

// ───── Server functions config ─────
// ───── Server Functions config ─────

/**
* Resource ceilings applied when decoding a client → server RSC reply
* (server function arguments).
* (Server Function arguments).
*
* These limits are enforced inside the reply decoder before any server
* function runs. They cap the cost of payload deserialization and protect
* the server from denial-of-service vectors that exploit the rich
* RSC reply wire format (deep nesting, huge BigInts, oversized streams,
* unbounded bound-argument lists, etc.).
* These limits are enforced inside the reply decoder before any
* Server Function runs. They cap the cost of payload deserialization
* and protect the server from denial-of-service vectors that exploit
* the rich RSC reply wire format (deep nesting, huge BigInts, oversized
* streams, unbounded bound-argument lists, etc.).
*
* Each limit is independent. A zero or negative value is treated as the
* default. Setting a value larger than the default loosens the ceiling;
* setting a smaller value tightens it.
*
* When a request exceeds any limit, the decoder throws a
* `DecodeLimitError` and the request is rejected before the server
* function is invoked.
* `DecodeLimitError` and the request is rejected before the
* Server Function is invoked.
*/
export interface ServerFunctionDecodeLimits {
/**
Expand All @@ -837,7 +837,7 @@ export interface ServerFunctionDecodeLimits {
maxBytes?: number;

/**
* Maximum number of bound arguments on a server reference.
* Maximum number of bound arguments on a Server Function reference.
* Matches React's upstream default.
* @default 256
*/
Expand Down Expand Up @@ -866,7 +866,7 @@ export interface ServerFunctionDecodeLimits {

export interface ServerFunctionsConfig {
/**
* Secret key for signing server function calls.
* Secret key for signing Server Function calls.
* @example `secret: "my-secret-key"`
*/
secret?: string;
Expand All @@ -890,7 +890,7 @@ export interface ServerFunctionsConfig {
previousSecretFiles?: string[];

/**
* Resource ceilings for decoding server function payloads.
* Resource ceilings for decoding Server Function payloads.
*
* Caps the maximum work the server will do per inbound RSC reply.
* Defaults match the @lazarv/rsc decoder's built-in safe ceilings,
Expand Down Expand Up @@ -1444,14 +1444,26 @@ export interface ReactServerConfig {
cache?: CacheConfig;

/**
* Server functions (RPC) configuration.
* @example `serverFunctions: { secret: "my-secret-key" }`
* Server Functions (RPC) configuration. Set to `false` to force-disable
* all Server Function processing — incoming POSTs are not decoded, the
* manifest is not queried, and the runtime falls through to normal page
* rendering. Object form configures crypto material and decode limits.
* @example `serverFunctions: { secret: "my-secret-key" } // or serverFunctions: false`
*/
serverFunctions?: ServerFunctionsConfig;
serverFunctions?: ServerFunctionsConfig | false;

/**
* Force-disable Remote Components rendering. Only `false` is meaningful —
* when set, the runtime ignores the `@__react_server_remote__` URL marker,
* the temporary-reference set is not created, and request bodies for
* Remote Components rendering are not decoded.
* @example `remoteComponents: false`
*/
remoteComponents?: false;

/**
* OpenTelemetry observability configuration.
* When enabled, the runtime instruments HTTP requests, rendering, server functions, and middleware.
* When enabled, the runtime instruments HTTP requests, rendering, Server Functions, and middleware.
* @example `telemetry: { enabled: true, serviceName: "my-app" }`
*/
telemetry?: TelemetryConfig;
Expand Down
18 changes: 14 additions & 4 deletions packages/react-server/config/schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export const DESCRIPTIONS = {
cookies: "Cookie options for the session.",
host: 'Host to listen on. Example: "0.0.0.0" or true (all interfaces).',
port: "Port to listen on (0–65535).",
remoteComponents:
"Force-disable Remote Components rendering. Only `false` is meaningful — when set, the runtime ignores the `@__react_server_remote__` URL marker and never enters the Remote Components code path. Apps that don't host Remote Components can set this for defense-in-depth.",
console: "Disable the dev console overlay.",
overlay: "Disable the dev error overlay.",
assetsInclude:
Expand Down Expand Up @@ -244,15 +246,16 @@ export const DESCRIPTIONS = {
"cache.providers": "Cache storage providers.",

// serverFunctions.*
serverFunctions: "Server functions (RPC) configuration.",
"serverFunctions.secret": "Secret key for signing server function calls.",
serverFunctions:
"Server Functions (RPC) configuration. Set to `false` to force-disable all Server Function processing — incoming POSTs are not decoded, the manifest is not queried, and the runtime falls through to normal page rendering. Object form configures crypto material and decode limits.",
"serverFunctions.secret": "Secret key for signing Server Function calls.",
"serverFunctions.secretFile": "Path to a file containing the secret key.",
"serverFunctions.previousSecrets":
"Previously used secrets for key rotation.",
"serverFunctions.previousSecretFiles":
"Previously used secret files for key rotation.",
"serverFunctions.limits":
"Resource ceilings for decoding server function payloads (per-request DoS protection).",
"Resource ceilings for decoding Server Function payloads (per-request DoS protection).",
"serverFunctions.limits.maxRows":
"Maximum number of outlined rows per reply. Default: 10000.",
"serverFunctions.limits.maxDepth":
Expand Down Expand Up @@ -288,7 +291,7 @@ export const DESCRIPTIONS = {

// telemetry.*
telemetry:
"OpenTelemetry observability configuration. When enabled, the runtime instruments HTTP requests, rendering, server functions, and middleware.",
"OpenTelemetry observability configuration. When enabled, the runtime instruments HTTP requests, rendering, Server Functions, and middleware.",
"telemetry.enabled":
"Enable/disable telemetry. Also enabled by OTEL_EXPORTER_OTLP_ENDPOINT or REACT_SERVER_TELEMETRY env vars.",
"telemetry.serviceName":
Expand Down Expand Up @@ -504,6 +507,7 @@ export function generateJsonSchema() {
cookies: prop({ type: "object" }, "cookies"),
host: prop({ oneOf: [{ type: "string" }, { const: true }] }, "host"),
port: prop({ type: "integer", minimum: 0, maximum: 65535 }, "port"),
remoteComponents: prop({ const: false }, "remoteComponents"),
console: prop({ type: "boolean" }, "console"),
overlay: prop({ type: "boolean" }, "overlay"),
assetsInclude: prop(
Expand Down Expand Up @@ -1017,6 +1021,12 @@ export function generateJsonSchema() {
),

// ── serverFunctions.* ──
// The runtime validator additionally accepts the literal `false`
// to force-disable all server-function processing — see
// config/validate.mjs (`falseOrShape`). The JSON schema below
// describes the canonical object form for IDE autocomplete; the
// boolean shortcut isn't surfaced here because most users opt out
// via JS config rather than JSON.
serverFunctions: prop(
{
type: "object",
Expand Down
37 changes: 35 additions & 2 deletions packages/react-server/config/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ function objectShape(shape) {
return fn;
}

/**
* Accepts the literal `false` OR an object that satisfies `shape`.
*
* Used for force-disable config keys like `serverFunctions: false` where
* the same key also accepts a richer object form for normal configuration.
*
* Returns a validator that passes for `false` or any object value.
* The `_shape` property is preserved so the engine's nested-shape recursion
* (validateObject) still validates inner keys when the value is an object.
* The recursion guard (`_shape && is.object(value)`) keeps `false` from
* being walked as an object.
*/
function falseOrShape(shape) {
const fn = (v) => v === false || is.object(v);
fn._shape = shape;
fn._allowFalse = true;
return fn;
}

function enumOf(...values) {
const fn = (v) => values.includes(v);
fn._enum = values;
Expand Down Expand Up @@ -212,6 +231,13 @@ const REACT_SERVER_SCHEMA = {
host: optional(oneOf(is.string, (v) => v === true)),
port: optional(is.number),

// Force-disable Remote Components rendering. When `false`, the runtime
// ignores the `@__react_server_remote__` URL marker, never enters the
// Remote Components code path, and never parses request bodies as
// Remote Components payloads. Any other value (or omitted) leaves
// Remote Components rendering enabled.
remoteComponents: optional((v) => v === false),

// ── Dev overlay / console ──
console: optional(is.boolean),
overlay: optional(is.boolean),
Expand Down Expand Up @@ -461,8 +487,14 @@ const REACT_SERVER_SCHEMA = {
),

// ── serverFunctions.* ──
// Accepts the existing object shape OR the boolean `false` to force-disable
// all Server Functions processing. When `false`, the runtime's action-dispatch
// block never executes, the request body is never parsed for action calls,
// and the manifest is never queried — the request flows through to normal
// page rendering. Useful for apps that have no Server Functions and want
// to eliminate the action-dispatch surface as a precaution.
serverFunctions: optional(
objectShape({
falseOrShape({
secret: optional(is.string),
secretFile: optional(is.string),
previousSecrets: optional(arrayOf(is.string)),
Expand Down Expand Up @@ -607,6 +639,7 @@ const EXAMPLES = {
cookies: `cookies: { secure: true, sameSite: "lax" }`,
host: `host: "0.0.0.0" // or true for all interfaces`,
port: `port: 3000`,
remoteComponents: `remoteComponents: false // disable Remote Components rendering`,
console: `console: false // disable dev console overlay`,
overlay: `overlay: false // disable dev error overlay`,
assetsInclude: `assetsInclude: ["**/*.gltf"] // or string | RegExp`,
Expand Down Expand Up @@ -714,7 +747,7 @@ const EXAMPLES = {
cache: `cache: { profiles: { ... }, providers: { ... } }`,
"cache.profiles": `cache: { profiles: { default: { ttl: 60 } } }`,
"cache.providers": `cache: { providers: { memory: { ... } } }`,
serverFunctions: `serverFunctions: { secret: "my-secret-key" }`,
serverFunctions: `serverFunctions: { secret: "my-secret-key" } // or serverFunctions: false to disable`,
"serverFunctions.secret": `serverFunctions: { secret: "my-secret-key" }`,
"serverFunctions.secretFile": `serverFunctions: { secretFile: "./secret.pem" }`,
"serverFunctions.previousSecrets": `serverFunctions: { previousSecrets: ["old-secret"] }`,
Expand Down
21 changes: 17 additions & 4 deletions packages/react-server/lib/dev/ssr-handler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,24 @@ export default async function ssrHandler(root) {
const isMultipart = !!httpContext.request.headers
.get("content-type")
?.includes("multipart/form-data");
// Mirror the feature gates render-rsc.jsx applies. When
// server functions or remote rendering are force-disabled
// via config, the corresponding request shapes get demoted
// to "regular request" here so we route them through the
// client-root SSR shortcut instead of activating the full
// RSC entry. Defense-in-depth: render-rsc.jsx re-checks the
// same gates, so even if a request slips through here it
// never reaches the action-dispatch code path.
const serverFunctionsEnabled =
configRoot.serverFunctions !== false;
const remoteEnabled = configRoot.remoteComponents !== false;
const isActionRequest =
isMutating && (hasActionHeader || isMultipart);
const isRemoteRequest = httpContext.url.pathname.includes(
"@__react_server_remote__"
);
serverFunctionsEnabled &&
isMutating &&
(hasActionHeader || isMultipart);
const isRemoteRequest =
remoteEnabled &&
httpContext.url.pathname.includes("@__react_server_remote__");
const entryModule =
isClientRoot && !isActionRequest && !isRemoteRequest
? clientRootEntryModule
Expand Down
Loading
Loading