LRLucas RochaLet’s talk
← All articles

Cyber Coffee · English edition

Modern frontend security: what changes with Next.js App Router and React 19

Server Components and Server Actions moved the boundary between client and server. Every time that boundary moves, the security surface moves with it.

  • AppSec
  • React
A trust boundary filters data between a browser and a server while a private key stays on the server.

Server Components and the App Router redesigned where frontend code actually runs. Previously, the mental model was simple: everything in the React bundle ran in the browser, full stop. Now, the same .tsx file can run on the server, on the client or on both. That ambiguity, powerful for performance, is exactly where new pitfalls appear that the old mental model does not cover.

Client/server confusion is a new source of leaks

With Server Components, it is easy to let a secret, an environment variable or a database query slip into the client bundle: a misplaced “use client” directive, a careless import of a module carrying an API key, or a component that started as server-only and was moved without anyone reviewing what it carried along.

The symptom is subtle: the code compiles, the application works, and the secret only becomes apparent if someone opens the final bundle and looks for it. There is no build error or obvious warning. Knowing exactly what runs where is no longer just a performance detail; it is a security control. Treating the server/client boundary as a trust boundary means reviewing every component that crosses it during code review, rather than assuming “it is all on the server” simply because the file does not have “use client” at the top.

Server Actions are endpoints: treat them that way

It is tempting to see a Server Action as “just a function that runs on the server,” called with the same syntax as a local function. In practice, however, it is a public endpoint disguised as a function call: anyone can construct a direct request to it without going through the UI that normally invokes it.

Every validation, authorization and sanitization measure that would apply to a traditional API route applies equally to a Server Action. That includes not trusting incoming arguments, even when the function’s TypeScript signature suggests a specific type. TypeScript guarantees shape at compile time; it guarantees nothing about what actually arrives at runtime from outside your control. The convenient syntax, “use server” and calling it like a function, hides the real surface: every exposed Server Action is a route that needs the same authentication and authorization checklist as any API endpoint.

Server rendering changes the sensitive-data equation

Data that previously existed only behind an API call, and therefore only reached the client when explicitly requested, can now be rendered directly on the server and embedded in the returned HTML. This is excellent for performance because it removes a network round trip, but dangerous if you do not deliberately filter what goes to the client: it is easy to simply pass the entire object held by the server to the rendering component.

Fetching a user from the database to render their name and inadvertently serializing the whole object, including a password hash, internal flags and fields that should never leave the backend, to a client component is a concrete, recurring mistake in this model. What the server knows is not what the client should see. Separation must be explicit, field by field, where data passes between layers, never implicit because “it is easier to pass the whole object.”

CSP and headers remain the floor, not the ceiling

No framework innovation replaces a properly configured Content-Security-Policy, security headers such as X-Frame-Options, Strict-Transport-Security and Referrer-Policy, or ongoing care with third-party dependencies in the bundle. The foundation that existed before the App Router still applies in full. The App Router adds new layers of risk on top of that foundation; it does not replace it.

Teams that migrate and consider the migration “done” on the day the application builds without errors are effectively ignoring that the frontend security checklist has grown, rather than shrunk.

What to take away

Migrating to the App Router is good architecture and worth the investment. But it reopens security decisions that lay dormant in the old client-heavy model, because that model had one obvious boundary, browser versus server, while the new one has a moving boundary decided file by file. Revisit each boundary explicitly: what runs where, what each Server Action validates and what each server component actually serializes to the client. Treating this as a one-off migration review instead of an ongoing discipline is how subtle leaks slip in.

← Back to all articles