Hardened Transport
Phaze Transport is a declarative capability stack — input validation, use[] middleware, the handler, then outbound flatbuffer: / sign: / encrypt: — where every concern is a field the compiler wires on both ends. That shape isn’t only an ergonomics win; it’s the security win. The most-cited prior art, Next.js Server Actions / React Server Functions, takes the opposite shape — an opaque RPC wire that auto-deserializes untrusted input behind a gate you can skip — and the consequences are a matter of public record. This page is the contrast, mechanism by mechanism.
The shape of the risk: an auto-deserializing RPC wire behind a bypassable gate
Section titled “The shape of the risk: an auto-deserializing RPC wire behind a bypassable gate”A Server Action is a publicly-callable endpoint. The client sends a serialized argument graph; the server deserializes it into execution. Two things follow from that shape, and both have CVEs:
| Risk class | What happened | Why the shape caused it |
|---|---|---|
| Insecure deserialization → RCE | CVE-2025-55182 (React Server Functions, CVSS 10.0) — unauthenticated remote code execution in the default configuration. Companions: a DoS (a crafted request loops forever during deserialization) and source-code exposure (a Server Function returns its own source when args are stringified). | The wire is a rich serialized object graph the runtime reconstructs server-side. Untrusted input steering reconstruction is the textbook insecure-deserialization sink. |
| Bypassable gate | CVE-2025-29927 — a request header (x-middleware-subrequest) let attackers skip middleware entirely, reaching _next/data payloads and admin routes past role checks. | Authorization lived in a separable middleware layer in front of the route. A separable layer is a skippable layer. |
| Authz-per-action drift | The standing guidance: “check auth in the page, but the Action is a separate endpoint.” A page guard does not protect the action reachable from it. | The action is decoupled from the page’s protection; the check is imperative, in the body, easy to forget. |
In short: the wire is opaque, the validation is optional, and the gate is detachable. Security is something you remember to add, in three different places, none enforced by the type system.
Phaze Transport: Next.js / React vulnerabilities can’t happen here
Section titled “Phaze Transport: Next.js / React vulnerabilities can’t happen here”Phaze does the opposite of each one above — and not because you turn something on. It’s simply how the layer works; there’s no other way to write it, and no unsafe mode you have to remember to avoid.
1 — The wire is data validated by a declared schema, never a deserialized execution graph
Section titled “1 — The wire is data validated by a declared schema, never a deserialized execution graph”A Phaze action’s wire is plain JSON (or a typed binary — application/octet-stream / application/x-flatbuf), and the handler’s first act is input.parse(body) against a declared zod schema:
export const transferFunds = newAction({ input: z.object({ to: z.string().uuid(), cents: z.number().int().positive() }), use: [requireSession], // ← authz is a field, runs before the handler sign: 'ed25519', // ← the response is signed handler: async ({ to, cents }, { env }) => { /* … */ },})There is no devalue / RSC object-graph reconstruction, no closure-captured variables serialized to the client, and no “deserialize untrusted input into execution.” Malformed input short-circuits to a 400 envelope before any handler code runs. The insecure-deserialization / DoS / source-leak class (CVE-2025-55182 and its companions) has no sink here — the wire is a value checked by a contract, not a program the server rebuilds.
2 — Authorization is compiled onto the action, not remembered in its body
Section titled “2 — Authorization is compiled onto the action, not remembered in its body”input (validation) and use[] (auth / verify) are fields you write right on the action, and they stay attached to it. The compiler bakes them into how the action runs, so it can’t execute without the checks you declared. The classic Next.js mistake — guarding the page but forgetting the action behind it — can’t happen here: every action checks itself.
3 — The page gate is non-bypassable because it isn’t a separable layer
Section titled “3 — The page gate is non-bypassable because it isn’t a separable layer”Phaze’s per-route restricted guard runs inside the route’s own server dispatch (core()), not in a middleware tier in front of it. There is no x-middleware-subrequest analog because there is no separable hop to skip — the authorization is the request’s data pass, resolved server-side at the matched route (“the secure check, close to the data”). And because phaze SSR-seeds the session, that check is the same pass that produces the page — one server round-trip, no detachable front layer to walk around.
How Phaze improves security over the React ecosystem with cryptographic transport
Section titled “How Phaze improves security over the React ecosystem with cryptographic transport”The structural wins above close Next’s holes. The sign: / verify: / encrypt: fields go further — they’re capabilities the Server Action model simply doesn’t have, and they cost zero shipped bytes for native algorithms (the browser verifies with inline subtle.verify — see Signing & verification).
| Mechanism | Phaze Transport | Next Server Actions |
|---|---|---|
| Response signing — browser verifies authenticity + integrity before reading; a tampered body throws | sign: 'ed25519' → detached X-Phaze-Sig over the exact bytes; inline native verify | none — responses are unsigned; integrity ends at TLS |
Signed SSR-seeded auth context — the inlined __PHAZE_CF__ session is tamper-evident (defends HTML-injection / cache-poisoning) and fetch-free | sign the seed; client verifies on hydrate | client useSession re-fetches over TLS; no app-level signature |
| Required signed invocation — a sensitive action demands a cryptographically signed request (proof of origin, not just a CSRF token) | inbound verify: (Rust verify = "Ed25519<…>"); server verifies before the handler | CSRF/Origin checks only — any client that passes them can call the action |
| Response encryption — declarative per-response confidentiality | encrypt: (native AES-GCM; PQC gated) | only build-key closure encryption (a leak risk), not a response field |
Post-quantum / hybrid (shipped on the Rust runtime: sign = "ed25519+ml-dsa" + encrypt = "ml-kem-768+aes-gcm" + compose, runtime-proven) — integrity + confidentiality for long-lived / at-rest data (KV sessions, PHI/EEG in R2) | sign: 'ed25519+ml-dsa', encrypt: 'ml-kem-768+aes-gcm' — browser checks the classical half native (0 WASM even hybrid), PQC server-side | none |
Defense in depth beyond TLS. TLS protects the live hop. sign: protects the payload — across CDNs, caches, R2, and untrusted relays — with the same X-Phaze-Sig wire whether a TS action or a Rust #[get] produced it (one model, two runtimes). The browser holds only the public key (PUBLIC_PHAZE_SIGN_KEY); the signing key (PHAZE_SIGN_KEY) is a worker secret.
sign: and encrypt: compose — sign-then-encrypt. When both are set the worker signs the plaintext, then encrypts the signed payload, so the wire frame is the opaque { e, iv } and the client decrypts first, then verifies. The two are not redundant: AES-GCM is an AEAD, so its tag rejects a tampered frame on decrypt (confidentiality + tamper-evidence); the Ed25519 signature adds server-origin proof the symmetric layer can’t — PUBLIC_PHAZE_ENCRYPT_KEY is client-shipped, so anyone holding it could mint a valid frame, but only the signature (private PHAZE_SIGN_KEY, never shipped) authenticates the producer across CDN / cache / relay. For a Rust-backed action the crypto splits across the runtime boundary: Rust signs natively, the TS dispatcher encrypts and preserves the X-Phaze-Sig header the browser verifies over the decrypted bytes (a Rust-side encrypt= attr isn’t built — encryption is the dispatcher’s job).
A sensitive mutation, side by side
Section titled “A sensitive mutation, side by side”// Next — the gate is elsewhere; the action trusts its input shape; the response is unsigned.'use server'export async function deleteAccount(formData: FormData) { // ⚠ if you forget THIS line, the page guard didn't protect this endpoint const session = await auth() if (!session) throw new Error('unauthorized') const id = formData.get('id') // untyped; arrives via the RSC wire await db.users.delete(id as string) return { ok: true } // unsigned — the client can't prove it's authentic}// Phaze Transport — validation, authz, and integrity are fields the compiler enforces.export const deleteAccount = newAction({ input: z.object({ id: z.string().uuid() }), // validated before the handler runs use: [requireSession], // authz carried by the action, not the page sign: 'ed25519', // the client verifies the result is authentic handler: async ({ id }, { env }) => { await env.DB.prepare('DELETE FROM users WHERE id = ?1').bind(id).run() return { ok: true } },})Same intent; the difference is where security lives. In Next it’s three imperative habits (re-check auth, coerce the input, trust the channel). In Phaze Transport it’s three fields the build wires onto the call — present or the action doesn’t compile the way you meant.
Passkeys close the loop on the credential
Section titled “Passkeys close the loop on the credential”Hardened transport protects the channel; passkeys (WebAuthn) harden the credential. They’re phishing-resistant and origin-bound (the assertion is cryptographically scoped to your RP ID — a lookalike domain can’t replay it), and there’s no shared secret to leak (unlike passwords / OTPs). Verification is ECDSA-P256 + CBOR/COSE on the Rust runtime — the same phaze-crypto that signs transport responses. Pair that with server-revocable KV sessions (delete the key — a stateless JWT cookie can’t be revoked before expiry) and the auth story is hardened end to end: credential, session, and channel.
FlatBuffer is a perf encoding — not a security field
Section titled “FlatBuffer is a perf encoding — not a security field”flatbuffer: is zero-copy encoding; it composes with sign: (the detached X-Phaze-Sig keeps the body a clean signed buffer) but adds no security of its own. Auth and session payloads are small JSON → JSON + sign:, no FlatBuffer needed. It earns its place only where a signed payload is also large / numeric / at-rest — a signed session manifest, or the MoQ replay case (flatbuffer: + encrypt: + per-chunk sign: over an untrusted relay). Reach for it there; not in the auth flow.
The scorecard
Section titled “The scorecard”| Next.js Server Actions / RSC | Phaze Transport | |
|---|---|---|
| Wire | serialized object graph, auto-deserialized server-side | typed JSON / binary, input.parse’d first |
| Input validation | optional, imperative | declared field (input:), enforced pre-handler |
| Authorization | imperative, per-action, easy to forget | declared field (use[]) carried by the action |
| Page gate | separable middleware (bypassable — CVE-2025-29927) | per-route restricted guard in core() — non-bypassable |
| Response integrity | none (TLS only) | sign: Ed25519, verified before reading |
| Response confidentiality | build-key closure encryption only | encrypt: (AES-GCM → PQC) |
| Signed invocation | CSRF/Origin only | verify: (Ed25519 request signature) |
| Post-quantum | none | hybrid sign:/encrypt:, 0 browser WASM (shipped on the Rust runtime; runtime-proven) |
| Deserialization RCE class | CVE-2025-55182 (10.0) | no sink — wire is data, not a program |
| Client bytes for native crypto | n/a (no signing) | 0 — inline subtle.verify |
See also
Section titled “See also”- Signing & verification — the
sign:/verify:mechanics,X-Phaze-Sig, key management, per-algo dispatch. - Passkeys (WebAuthn) — the phishing-resistant credential half.
- Post-quantum & at-rest — hybrid signatures and the harvest-now-decrypt-later model.
- Phaze Transport — the capability stack these fields live on.