Seus dados aparecem mesmo com redirect() no layout do Next.js
Fui revisar um projeto com área de admin protegida e encontrei dados sensíveis sendo entregues via __next_f.push mesmo com isAdmin() e redirect() no layout. Isso não é bug — é arquitetura. E acontece em produção hoje.
Tem um padrão que aparece em praticamente todo projeto Next.js com App Router que eu já revisei. Parece correto, o TypeScript não reclama, o LLM gerou esse código pra você, a documentação não avisa. E ele vaza dados sensíveis pra qualquer pessoa que abrir o DevTools.
Como eu encontrei isso
Estava fazendo uma revisão de código num projeto com App Router. Tinha uma área de admin protegida — relatórios, dados de usuários, aquele tipo de coisa. O código de proteção era esse aqui, bem padrão:
// app/admin/layout.tsx
import { redirect } from 'next/navigation'
import { isAdmin } from '@/lib/auth'
export default async function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
const admin = await isAdmin()
if (!admin) {
redirect('/login')
}
return <>{children}</>
}
Funciona, né? Usuário não admin bate no layout, chama o redirect(), vai pro /login. Parece seguro.
Fui checar o comportamento em navegação client-side: abri o DevTools, filtrei por fetch, naveguei até /admin/relatorios sem estar logado como admin, e encontrei isso:
1:HL["/_next/static/chunks/page.js","script"] 0:["$","div",null, ["$","h1",null,"Relatório Q1 2024"], ["$","p",null,"Receita total: R$ 847.293,00"], ["$","table",null, ← dados de usuários aqui ←] ]
Status 200. Payload completo. Dados sensíveis entregues pro cliente. O redirect ainda vai acontecer — mas só depois, quando os dados já foram.
Por que isso acontece
O Next.js App Router renderiza layouts e páginas de forma concorrente. Quando você faz uma navegação client-side, o framework começa a renderizar a árvore inteira — layout e página — ao mesmo tempo, em paralelo.
Olha como o fluxo funciona na prática:
O redirect() cancela a renderização no servidor, mas o payload RSC já foi streamado ao cliente antes disso.
O redirect() do Next.js lança uma exceção interna que interrompe a renderização. O problema é que em navegações client-side, o React Server Components usa um protocolo de streaming chamado RSC Flight — e partes da árvore de componentes já foram serializadas e empurradas pro cliente antes da exceção do layout ser processada.
É o __next_f.push que você vê no source da página ou no payload das requests. Ele é o “fio” pelo qual o Next.js entrega componentes serializados de forma incremental. Você pode ver isso abrindo o source de qualquer página Next.js e procurando por __next_f — todos os dados dos Server Components estão ali, em texto, antes mesmo do React hidratar o DOM.
O código que parece certo mas não é
Esse padrão é incrivelmente comum. Olha quantas formas diferentes de escrever a mesma coisa errada:
// ❌ Versão 1 — a mais comum
export default async function AdminLayout({ children }) {
const session = await getServerSession()
if (!session?.user?.isAdmin) redirect('/login')
return <>{children}</>
}
// ❌ Versão 2 — com cookie
export default async function DashboardLayout({ children }) {
const token = cookies().get('auth-token')
const user = await verifyToken(token?.value)
if (!user) redirect('/unauthorized')
return <main>{children}</main>
}
// ❌ Versão 3 — parece mais robusta mas tem o mesmo problema
export default async function ProtectedLayout({ children }) {
const { userId } = auth() // Clerk, por exemplo
if (!userId) redirect('/sign-in')
return <SidebarLayout>{children}</SidebarLayout>
}
Todas têm o mesmo problema. O layout redireciona, mas a página já começou a ser renderizada concorrentemente.
Por que todo mundo escreve assim
Três razões:
A documentação demorou pra avisar — e ainda é fácil de ignorar. Por muito tempo, a doc oficial do Next.js mostrava exemplos de auth em layouts sem nenhum alerta. A Vercel adicionou uma seção específica sobre isso no guia de autenticação, com o seguinte aviso:
“Due to Partial Rendering, you should be cautious when doing checks in Layouts as these don’t re-render on navigation, meaning the user session won’t be checked on every route change.”
O problema é que o aviso está enterrado num guia longo, o framing é sobre “sessão não re-checada em cada rota” — que soa como um detalhe menor — e não menciona diretamente o vazamento de dados via RSC payload. Quem lê rápido passa por cima.
Os LLMs geram esse padrão constantemente. Pedi pra três modelos diferentes mostrarem como proteger uma rota no App Router. Todos os três me deram auth no layout como primeira sugestão. É o padrão que mais aparece em tutoriais indexados, e os modelos aprenderam com esses tutoriais. A doc nova ainda não chegou no corpus de treinamento da maioria deles.
Funciona perfeitamente em navegação direta (hard navigation). Se você acessa a URL diretamente pelo browser, o servidor processa tudo antes de enviar qualquer HTML, então o redirect funciona como esperado. O problema aparece só em navegações client-side — o que é, ironicamente, a maioria das navegações em um SPA moderno.
O que aparece no DevTools
Quando o Next.js faz uma navegação client-side, ele busca o RSC payload via fetch. A response tem content-type: text/x-component e parece com isso:
1:HL["/_next/static/chunks/admin-page.js","script"]
2:I["./components/RelatorioTable.tsx",["relatorio"],"RelatorioTable"]
0:["$","div",null,{
"className":"admin-content",
"children":[
["$","h1",null,{"children":"Painel Financeiro"}],
["$","$L2",null,{
"data": {
"receita": 847293.00,
"usuarios": [{"id":"usr_1","email":"[email protected]","plano":"enterprise"}],
"churnRate": 0.034
}
}]
]
}]
Esse é o RSC Flight format. Cada linha é um “chunk” da árvore de componentes. Os dados que você buscou do banco — aqueles que você acha que estão protegidos pelo layout — estão ali, em texto, visíveis na aba Network.
Não precisa de nenhuma ferramenta especial. Só abrir o DevTools.
Isso foi corrigido nas versões mais recentes?
Testei com Next.js 15 e 16 e a resposta curta é: não, porque não é um bug.
É uma característica do modelo de renderização do App Router. O React Server Components foi desenhado pra renderizar de forma concorrente e fazer streaming de partes da UI à medida que ficam prontas. O layout e a página são partes da mesma árvore e começam a ser renderizados juntos.
O redirect() cancela a árvore no servidor quando é processado, mas dependendo do timing da sua chamada assíncrona (await isAdmin()), partes da página já podem ter sido serializadas. Em versões mais recentes o comportamento é um pouco mais determinístico em alguns casos, mas a garantia de segurança não existe.
A própria Vercel agora reconhece isso na doc oficial — a seção sobre layouts e auth checks orienta explicitamente a não depender só de layouts para autorização e a fazer os checks próximos à fonte de dados. Portanto: não é um bug que vai ser corrigido, é um comportamento intencional que exige que você escreva o código certo.
O contexto ficou mais pesado ainda no final de 2025, quando foram divulgados CVE-2025-55182 e CVE-2025-66478, duas vulnerabilidades críticas no protocolo RSC que permitiam execução remota de código. A Vercel publicou um security update com versões corrigidas. Se você ainda está em versões antigas, atualiza primeiro — e depois corrige o auth.
Como fazer certo
Tem três abordagens que realmente funcionam:
1. Checar auth na página, não no layout
// ✅ A checagem fica onde os dados estão
// app/admin/relatorios/page.tsx
export default async function RelatoriosPage() {
const admin = await isAdmin()
if (!admin) redirect('/login')
// só chega aqui se for admin — fetch e check na mesma função
const data = await getRelatorios()
return <RelatoriosView data={data} />
}
Quando a checagem e o fetch de dados estão na mesma função, o Next.js não tem como serializar os dados antes de executar o redirect.
2. Usar middleware para proteger rotas inteiras
// middleware.ts — na raiz do projeto
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*'],
}
O middleware roda antes de qualquer renderização. Nenhum dado de página é tocado. É o lugar certo pra redirecionamentos de auth.
3. Combinar as duas abordagens
Na prática você vai querer middleware pra proteção de rota (impede qualquer request de chegar à página) mais checagem na página pra autorização granular (não basta estar logado, precisa ter a permissão certa):
// middleware.ts — barra usuários não logados
export function middleware(request: NextRequest) {
const session = request.cookies.get('session')
if (!session && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
// app/admin/relatorios/page.tsx — barra usuários sem permissão
export default async function RelatoriosPage() {
const user = await getCurrentUser()
if (!user?.permissions.includes('view:relatorios')) {
redirect('/admin') // logado mas sem permissão
}
const data = await getRelatorios()
return <RelatoriosView data={data} />
}
Resumindo
O layout no App Router não é um portão de segurança. É um wrapper de UI. Usar redirect() no layout parece razoável, funciona em hard navigation, os testes passam, o colega não reclama — até alguém abrir o DevTools.
A correção é simples: move a checagem de permissão pra mesma função que faz o fetch dos dados sensíveis. Se você usa middleware pra auth básica, ainda sim faça a checagem de autorização na página.
Revisita os layouts do seu projeto. Provavelmente vai encontrar pelo menos um assim.