Arquitetura de Segurança em Aplicações Frontend: Server Actions e Proteção de Dados na Era Next.js
A evolução do desenvolvimento frontend nos últimos anos foi radical. Enquanto há cinco anos, aplicações de página única (SPA), onde toda a lógica era executada no navegador e o servidor era simplesmente uma REST API, eram o padrão de fato, hoje observamos uma transição em massa para arquiteturas híbridas. Next.js, com seus Server Components e Server Actions, tornou-se não apenas um framework popular, mas um padrão da indústria para aplicações empresariais.
Essa transição trouxe consigo muitas vantagens: melhor desempenho, melhor SEO, desenvolvimento simplificado. No entanto, também mudou o modelo de ameaças que os desenvolvedores enfrentam. Métodos de proteção familiares, baseados em JWTs em cabeçalhos e políticas CORS, não garantem mais segurança total. A lógica do servidor agora é executada nas proximidades do cliente, e a fronteira entre o frontend e o backend se tornou difusa (para alguns cenários).
De acordo com pesquisas da Snyk e outros fornecedores de segurança, 39% dos ativos em nuvem continham versões vulneráveis de React e Next.js em 2024-2025. Isso não é apenas estatística. São aplicações reais, processando dados do usuário, informações de pagamento e dados confidenciais de negócios. A vulnerabilidade CVE-2025-55182, que recebeu a classificação CVSS máxima de 10.0, mostrou o quão críticas podem ser as consequências da atenção insuficiente à segurança em aplicações frontend modernas.
React Server Components (RSC) tornou-se um novo padrão, mas com eles vieram novos vetores de ataque. Server Actions, que fornecem uma maneira conveniente de chamar a lógica do servidor diretamente dos componentes, são, na verdade, endpoints HTTP públicos. Se configurados incorretamente, eles podem se tornar uma brecha para invasores. A abordagem tradicional de security through obscurity não funciona aqui: ocultar endpoints não protegerá contra enumeração direcionada.
Neste artigo, analisaremos abordagens arquiteturais para garantir a segurança em aplicações Next.js. Não recomendações superficiais como "use HTTPS", mas padrões práticos que podem ser aplicados hoje. Analisaremos mecanismos de proteção em todos os níveis: da validação de entrada a medidas de infraestrutura.
Server Actions como um vetor de ataque
Server Actions é um dos recursos mais notáveis do Next.js moderno. Eles permitem que você escreva lógica do servidor diretamente em componentes, chame-os de formulários e manipuladores de eventos, sem criar endpoints de API explícitos. À primeira vista, isso parece mágica: escrevemos uma função com a diretiva use server, importamos no cliente e tudo funciona.
Mas por trás dessa conveniência, há um momento arquitetural importante. Cada Server Action é um endpoint HTTP POST público. Next.js cria automaticamente uma rota para chamar essa função, e essa rota está disponível externamente. Mesmo que a função não seja exportada explicitamente de page.tsx, ela ainda se torna parte da API pública de sua aplicação.
Isso cria o primeiro problema sério: contornar o middleware. Tradicionalmente, os desenvolvedores usam o middleware Next.js para proteger rotas - verificar autenticação, direitos de acesso, limitação de taxa. Mas Server Actions são chamados através de rotas internas especiais que podem não estar sujeitas ao middleware padrão. Isso significa que a verificação de sessão no middleware não garante a proteção da Server Action.
O segundo problema é a publicação implícita da lógica de negócios. Quando escrevemos uma Server Action, é fácil esquecer que essa função estará disponível para chamada direta. O desenvolvedor pode presumir que a função será chamada apenas de um determinado formulário com certas restrições. Mas um invasor pode estudar o pacote JavaScript do cliente, encontrar o nome e a assinatura da Server Action e chamá-la diretamente com argumentos arbitrários.
Considere um exemplo inseguro:
javascript// app/actions/user.ts "use server"; export async function updateUser(data: any) { await db.user.update({ where: { id: data.id }, data: { name: data.name, email: data.email, role: data.role, }, }); }
À primeira vista, o código parece funcionar. Mas aqui estão vários problemas de uma vez:
- Não há validação de entrada - o parâmetro data é do tipo any.
- Não há verificação de autenticação - qualquer pessoa pode chamar essa função.
- Não há verificação de autorização - o usuário pode atualizar qualquer perfil, incluindo o de outra pessoa.
- Não há restrição nos campos modificáveis - o campo role pode ser alterado por um usuário comum.
Agora, vamos dar uma olhada em uma implementação segura:
javascript// app/actions/user.ts "use server"; import { z } from "zod"; import { getSession } from "@/lib/auth"; import { canUpdateUser, canUpdateUserRole } from "@/lib/permissions"; const updateUserSchema = z.object({ id: z.string().uuid(), name: z.string().min(1).max(100).optional(), email: z.string().email().optional(), }); export async function updateUser(rawData: unknown) { const session = await getSession(); if (!session) { throw new Error("Unauthorized"); } const data = updateUserSchema.parse(rawData); if (!canUpdateUser(session.userId, data.id)) { throw new Error("Forbidden"); } const user = await db.user.update({ where: { id: data.id }, data: { name: data.name, email: data.email, }, select: { id: true, name: true, email: true, role: true, }, }); return user; }
O que mudou na versão segura:
- Adicionado um esquema de validação usando Zod - apenas os campos esperados, com os tipos e restrições corretos.
- Verificação explícita da sessão antes de executar qualquer lógica.
- Verificação de direitos de acesso - a função canUpdateUser determina se o usuário atual pode editar o perfil especificado.
- Restrição de campos modificáveis - role é excluído do esquema de atualização.
- Especificação explícita dos campos retornados - prevenção de vazamento acidental de dados.
Este padrão pode ser chamado de desenvolvimento de endpoint de API padrão para desenvolvedores frontend. Cada Server Action deve ser autossuficiente em termos de segurança: não confiar que será chamado do lugar "certo", mas verificar explicitamente todos os dados de entrada e o contexto de execução.
Zod e TypeScript - Defense in Depth
TypeScript tornou-se o padrão do desenvolvimento frontend moderno. Ele ajuda a detectar erros no estágio de compilação, fornece preenchimento automático no IDE e torna o código mais sustentável. No entanto, ao trabalhar com Server Actions, apenas TypeScript não é suficiente.
TypeScript funciona apenas no estágio de compilação. Após a compilação para produção, todos os tipos são apagados - JavaScript não tem informações de tipo em tempo de execução. Isso significa que se um invasor enviar dados do tipo errado para uma Server Action, o TypeScript não poderá impedir isso.
É aqui que a abordagem Defense in Depth (defesa em profundidade) entra em jogo. Usamos TypeScript para verificar no estágio de desenvolvimento e validação em tempo de execução para verificar no estágio de execução. Isso cria duas camadas de proteção independentes.
Zod é uma das bibliotecas mais populares para validação de esquema no ecossistema TypeScript. Ele permite definir esquemas de dados que inferem automaticamente os tipos TypeScript. Isso significa que temos uma única fonte de verdade para validação e tipagem.
javascriptimport { z } from "zod"; // Define o esquema const userSchema = z.object({ id: z.string().uuid(), name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().min(0).max(150).optional(), role: z.enum(["user", "admin", "moderator"]), }); // Inferir o tipo do esquema type User = z.infer<typeof userSchema>; // Usar em Server Action export async function createUser(rawData: unknown) { const data = userSchema.parse(rawData); // Agora data tem o tipo User e é garantido que passou na validação return db.user.create({ data }); }
As vantagens dessa abordagem são óbvias:
- A validação em tempo de execução garante que os dados correspondam às expectativas.
- A inferência automática de tipos exclui a dessincronização entre o esquema e os tipos.
- A escrita declarativa do esquema torna o código legível.
- Mensagens de erro integradas ajudam na depuração.
Para Server Actions, existe uma biblioteca especializada next-safe-action. Ele fornece um wrapper type-safe sobre Server Actions com validação integrada:
javascriptimport { createSafeActionClient } from "next-safe-action"; import { z } from "zod"; const actionClient = createSafeActionClient(); export const updateUserAction = actionClient .schema( z.object({ id: z.string().uuid(), name: z.string().min(1), }), ) .action(async ({ parsedInput }) => { // parsedInput já é validado e tipado return db.user.update({ where: { id: parsedInput.id }, data: { name: parsedInput.name }, }); });
next-safe-action lida automaticamente com erros de validação, retorna respostas tipadas e se integra ao sistema de tipos Next.js. Isso permite que você se concentre na lógica de negócios sem se preocupar com a rotina de validação.
É importante entender que a validação de entrada é apenas o primeiro nível de proteção. Mesmo que os dados tenham passado na verificação do esquema, isso não significa que sejam seguros do ponto de vista da lógica de negócios. Por exemplo, uma string que corresponde ao formato de email ainda pode pertencer a outro usuário ou ser bloqueada. Portanto, após a validação do esquema, as verificações de negócios são sempre necessárias.
Prevenção de vazamento de dados confidenciais
Um dos problemas mais sutis em uma arquitetura com Server Components é o vazamento acidental de dados confidenciais para o cliente. Em SPAs tradicionais, a separação era clara: o servidor só envia o que o cliente precisa. No Next.js com Server Components, a fronteira é difusa, e é fácil passar erroneamente para um componente do cliente dados que devem permanecer no servidor.
Abordagem atual (React 18 e Next.js 14)
Nas versões atuais, uma combinação de padrões é usada para evitar vazamentos:
- Separação de componentes do cliente e do servidor. Componentes com use client recebem apenas os dados que são explicitamente passados por meio de props. Server Components podem acessar o banco de dados diretamente, mas seu resultado de renderização é enviado ao cliente como HTML ou dados serializados.
- Pacotes somente para servidor. Next.js fornece a capacidade de marcar um módulo como somente para servidor, o que impedirá sua importação em componentes do cliente:
javascript// lib/database.ts import "server-only"; export async function getUserWithSecrets(id: string) { return db.user.findUnique({ where: { id }, include: { apiKeys: true, internalNotes: true }, }); }
Se você tentar importar essa função em um componente do cliente, a compilação falhará.
- DTOs (Data Transfer Objects). Restrição explícita dos campos passados para o cliente:
javascriptasync function getUserPublicProfile(id: string) { const user = await db.user.findUnique({ where: { id } }); // Retorna apenas campos públicos return { id: user.id, name: user.name, avatar: user.avatar, // apiKey, email, phone - excluídos intencionalmente }; }
A limitação dessa abordagem é que ela depende da disciplina do desenvolvedor. Não há verificação em tempo de execução que impeça a transmissão acidental de dados confidenciais.
O futuro: React 19 e Taint API
React 19 introduz a Taint API experimental, que adiciona proteção em tempo de execução contra vazamento de dados confidenciais. Este mecanismo permite "marcar" certos valores como confidenciais, e o React lançará um erro ao tentar passá-los para um componente do cliente.
javascriptimport { experimental_taintUniqueValue, experimental_taintObjectReference, } from "react"; // Marca um valor único (senha, chave de API) experimental_taintUniqueValue( "A senha não pode ser passada para o cliente", process.env.ADMIN_PASSWORD, ); // Marca o objeto inteiro const sensitiveConfig = { apiKey: process.env.API_KEY, dbUrl: process.env.DATABASE_URL, }; experimental_taintObjectReference( "A configuração contém segredos", sensitiveConfig, ); // Ao tentar passar esses dados para um componente do cliente // React lançará um erro durante a renderização
Isso aumenta significativamente o nível de proteção: mesmo que o desenvolvedor tente acidentalmente passar um segredo para o cliente, o aplicativo não quebra a produção, mas relata um erro no estágio de desenvolvimento ou durante a execução.
Lições da CVE-2025-55182
Dezembro de 2025 foi um lembrete da importância de atualizações de segurança oportunas. A vulnerabilidade CVE-2025-55182 afetou o React Server Components e recebeu a classificação de criticidade CVSS máxima de 10.0. Foi uma vulnerabilidade RCE (Remote Code Execution), que permitiu que um invasor executasse código arbitrário no servidor.
As versões vulneráveis foram React 19.0.0-19.2.0 e Next.js 15/16 antes dos patches correspondentes. Versões corrigidas: React 19.2.1+, Next.js 15.1.9+, 16.2.2+
O que é importante entender: essa vulnerabilidade não foi consequência de configuração incorreta por parte dos desenvolvedores. Estava no próprio framework. Mas as consequências para aplicações que não foram atualizadas a tempo poderiam ser catastróficas.
Recomendações práticas para usar o React 19 com segurança:
- Use apenas versões corrigidas (19.2.1+).
- Implemente a Taint API para dados críticos.
- Configure notificações automáticas sobre novas CVEs para dependências.
- Tenha um plano de atualização de emergência em caso de vulnerabilidades críticas.
Recomendações de migração
Se você planeja migrar para React 19 e Next.js 15+, é importante:
- Auditar todos os locais de transferência de dados entre Server e Client Components.
- Identificar todos os dados confidenciais que podem acidentalmente acabar no cliente.
- Implementar o padrão DTO em todos os lugares onde o vazamento é possível.
- Use a Taint API para proteção adicional.
- Atualize a política de monitoramento de alertas de segurança.
RBAC e controle de acesso no servidor
O problema do controle de acesso no Next.js é agravado pelo fato de que o middleware, que funciona no Edge, não tem acesso a todos os dados da sessão. Os desenvolvedores costumam confiar apenas no middleware para proteger rotas, mas isso não é suficiente para aplicações sérias.
O problema da enumeração de ID
A vulnerabilidade clássica em aplicações web é a enumeração de ID (iteração de identificadores). Se a aplicação usa IDs numéricos sequenciais e não verifica os direitos de acesso, um invasor pode iterar os IDs e obter acesso aos dados de outras pessoas:
javascript// Código vulnerável export async function getDocument(id: string) { // Não há verificação se o usuário atual tem o direito // obter este documento return db.document.findUnique({ where: { id } }); }
Com essa implementação, qualquer usuário autenticado pode obter qualquer documento simplesmente iterando o ID.
Por que o middleware-only não é suficiente
O middleware Next.js é executado no Edge antes de processar a solicitação. É bom para:
- Verificar a presença de uma sessão.
- Redirecionamentos de usuários não autenticados.
- Verificações simples no nível da rota.
Mas tem limitações:
- Não há acesso a informações completas sobre o usuário do banco de dados.
- Não há contexto do recurso específico (qual documento específico está sendo solicitado).
- É impossível verificar regras de acesso complexas de negócios.
Padrão de camada proxy de autorização
A solução é implementar a autorização diretamente em Server Actions e Server Components. Cada função que trabalha com dados deve verificar os direitos de acesso por conta própria.
javascript// lib/authorization.ts import { getSession } from "./auth"; export async function authorizeDocumentAccess(documentId: string) { const session = await getSession(); if (!session) { throw new Error("Unauthorized"); } const document = await db.document.findUnique({ where: { id: documentId }, include: { members: true }, }); if (!document) { throw new Error("Not found"); } const hasAccess = document.ownerId === session.userId || document.members.some((m) => m.userId === session.userId); if (!hasAccess) { throw new Error("Forbidden"); } return document; } // app/actions/documents.ts export async function updateDocument( documentId: string, data: UpdateDocumentData, ) { // A autorização é verificada dentro da ação const document = await authorizeDocumentAccess(documentId); // Verificação adicional dos direitos de edição if (document.ownerId !== (await getSession())?.userId) { throw new Error("Only owner can edit"); } return db.document.update({ where: { id: documentId }, data, }); }
Este padrão pode ser representado como uma arquitetura:
Client Request | Server Action / Server Component | Auth Check (sessão) | Authorization Layer (direitos sobre o recurso) | Business Logic
Cada nível é responsável por seu próprio aspecto de segurança:
- Auth Check confirma que a solicitação vem de um usuário autenticado.
- Authorization Layer verifica se esse usuário tem o direito de trabalhar com um recurso específico.
- Business Logic executa a operação, sabendo que todas as verificações foram aprovadas.
Modelo de acesso baseado em função (RBAC)
Para aplicações complexas, um modelo baseado em função é útil. As funções definem um conjunto de permissões, e os usuários recebem uma ou mais funções:
javascript// lib/rbac.ts const permissions = { document: { read: ["user", "admin", "viewer"], create: ["user", "admin"], update: ["admin", "owner"], delete: ["admin"], }, } as const; export function hasPermission( userRole: string, resource: keyof typeof permissions, action: string, ): boolean { const allowedRoles = permissions[resource][action] || []; return allowedRoles.includes(userRole); } // Usando em Server Action export async function deleteDocument(documentId: string) { const session = await getSession(); if (!session || !hasPermission(session.role, "document", "delete")) { throw new Error("Forbidden"); } // ... execução da exclusão }
Medidas de proteção de infraestrutura
Além dos padrões arquiteturais de código, as medidas de proteção de infraestrutura são importantes. Next.js fornece mecanismos integrados para proteger contra ataques comuns.
Proteção CSRF
Cross-Site Request Forgery (CSRF) é um ataque no qual um invasor faz com que o navegador do usuário execute uma ação indesejada no site onde o usuário está autenticado.
Next.js protege contra CSRF "out of the box":
- Verificação de origem. Server Actions verificam o cabeçalho Origin para garantir que a solicitação venha do mesmo domínio.
- Cookies SameSite. Com a configuração correta, os cookies com a flag SameSite=Strict ou SameSite=Lax não serão enviados em solicitações de origem cruzada.
javascript// middleware.ts import { NextResponse } from "next/server"; export function middleware(request: NextRequest) { const response = NextResponse.next(); // Cabeçalhos de segurança adicionais response.headers.set("X-Frame-Options", "DENY"); response.headers.set("X-Content-Type-Options", "nosniff"); response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); return response; }
Limitação de taxa no Edge A proteção contra ataques DoS e enumeração de senhas é implementada por meio da limitação de taxa. Next.js permite implementá-lo no nível das funções Edge para atraso mínimo:
javascript// middleware.ts import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, "1 m"), }); export async function middleware(request: NextRequest) { const ip = request.ip ?? "127.0.0.1"; const { success } = await ratelimit.limit(ip); if (!success) { return new NextResponse("Too many requests", { status: 429 }); } return NextResponse.next(); } export const config = { matcher: ["/api/:path*", "/app/actions/:path*"], };
Para Server Actions, a limitação de taxa pode ser implementada no nível de funções individuais:
javascriptimport { rateLimit } from "@/lib/rate-limit"; export async function sensitiveAction(data: unknown) { // Limite de taxa por userId ou IP await rateLimit({ key: "sensitive-action", limit: 5, window: 3600, // 1 hora }); // ... lógica principal }
Content Security Policy
Content Security Policy (CSP) é um cabeçalho que controla quais recursos uma página pode carregar. Isso protege contra ataques XSS e injeção de código de terceiros.
No Next.js, o CSP pode ser configurado por meio de middleware:
javascript// middleware.ts export function middleware(request: NextRequest) { const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); const cspHeader = ` default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'nonce-${nonce}'; img-src 'self' blob: data:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; ` .replace(/\s{2,}/g, " ") .trim(); const response = NextResponse.next(); response.headers.set("Content-Security-Policy", cspHeader); response.headers.set("x-nonce", nonce); return response; }
Para scripts inline em Server Components, um nonce é usado:
javascriptimport { headers } from 'next/headers' export default function Page() { const nonce = headers().get('x-nonce') return ( <script nonce={nonce}> {'console.log("Trusted inline script")'} </script> ) }
Conclusão
A segurança no frontend moderno exige uma reavaliação das abordagens familiares. A transição de SPAs para a arquitetura híbrida Next.js mudou não apenas a forma como o desenvolvimento é feito, mas também o modelo de ameaças. Server Actions, React Server Components e novas APIs exigem uma abordagem consciente à proteção.
Vamos resumir:
- Server Actions são endpoints HTTP públicos e exigem validação explícita de entrada e verificação de autorização dentro de cada função.
- TypeScript protege apenas no estágio de compilação. Para proteção em tempo de execução, a validação do esquema é necessária usando Zod ou bibliotecas semelhantes, criando uma abordagem Defense in Depth.
- React 19 introduz a Taint API para proteção em tempo de execução contra vazamento de dados confidenciais. Esta é uma adição aos padrões existentes de módulos somente para servidor e DTO.
- CVE-2025-55182 demonstrou a importância crítica de atualizações de segurança oportunas. Usar React 19.2.1+ e Next.js 15.1.9+ é obrigatório para segurança.
- A proteção somente para middleware é inadequada. Cada Server Action deve verificar de forma independente os direitos de acesso a recursos específicos por meio do padrão de camada proxy de autorização.
O efeito de negócios da implementação dessas práticas é medido não apenas na prevenção de incidentes, mas também nos riscos de reputação. Para aplicações empresariais que trabalham com dados confidenciais, a segurança não é um recurso opcional, mas um requisito fundamental.
O que vem a seguir
Em primeiro lugar, recomenda-se realizar uma auditoria de segurança de sua aplicação Next.js. Verifique se as Server Actions têm validação de dados e autorização, certifique-se de que não há vazamento de dados confidenciais em componentes do cliente, atualize as dependências para versões corrigidas (React 19.2.1+, Next.js 15.1.9+).
Compartilhe nos comentários quais padrões de segurança você usa em seus projetos? Quais ferramentas de automação de verificações de segurança você considera mais eficazes?





