Pular para o conteúdo

Noções de segurança

Variáveis de ambiente do Vite expostas: o prefixo diz publique isto

Variáveis de ambiente do Vite expostas no seu app: fizeram o que o prefixo pediu. VITE_ e NEXT_PUBLIC_ significam publique isto, e a IA nunca soube o custo.

Vlad Tkachenko11 min de leitura
Um arquivo .env com três valores, dois com a etiqueta VITE_, e esses dois valores desenhados de novo numa janela de navegador ao lado.

Em resumo

  • As variáveis de ambiente do Vite expostas no seu app estão lá porque o prefixo pediu. Uma variável chamada VITE_ ou NEXT_PUBLIC_ é copiada para o JavaScript que cada visitante baixa, e guardá-la em um arquivo .env não impede nada disso.
  • O prefixo é a etiqueta certa para um endereço ou uma chave publicável, e a errada para qualquer coisa que gaste dinheiro ou ignore as regras do seu banco de dados. O build não distingue as duas, e a IA que escreveu a linha também não.
  • De 30.998 apps vibe-coded no ar que escaneamos, 1.332 entregavam algo com cara de chave. 1.142 delas eram chaves de API do Google, que normalmente precisam de uma restrição e não de rotação. 52 entregavam uma chave que gasta dinheiro ou lê tudo.

Alguém abriu o seu app do Lovable, apertou F12 e encontrou no código um valor que você tem certeza de ter colocado em um arquivo .env. Ou você pediu uma função ao builder, ele escreveu uma linha começando com VITE_, e algo que você leu desde então diz que esse prefixo é por onde as chaves vazam. O arquivo tem .env no nome e todo tutorial diz para nunca compartilhá-lo. Variáveis de ambiente do Vite expostas a cada visitante soam como um bug do Vite.

Aqui está a parte que guia após guia explica errado: nada falhou em esconder esse valor. VITE_ e NEXT_PUBLIC_ são uma instrução para a sua ferramenta de build, e a instrução é colocar o valor no app que cada visitante baixa. A ferramenta leu a etiqueta e fez o que ela dizia.

Um build empacota o seu app em uma caixa, e cada visitante recebe uma cópia da caixa. O prefixo é uma etiqueta em um valor que diz: empacote isto também. O resto deste artigo é sobre para quais valores essa etiqueta está certa, por que a IA a coloca nos errados, e como ver o que está na sua própria caixa.

As variáveis de ambiente do Vite ficam expostas aos visitantes?

As que começam com VITE_, sim, e é para isso que o prefixo existe.

O Vite, a ferramenta de build por trás da maioria dos apps do Lovable e do Bolt, lê o seu arquivo .env toda vez que faz o build. Uma variável chamada VITE_SUPABASE_URL é copiada para o bundle, o arquivo JavaScript comprimido que cada visitante baixa, onde o seu código a lê como import.meta.env.VITE_SUPABASE_URL. Uma variável chamada DB_PASSWORD, sem prefixo, chega vazia ao navegador. A própria documentação do Vite diz que os valores com prefixo são empacotados no seu código-fonte na hora do build e não deveriam conter chaves de API.

O Next.js, com o qual o v0 constrói, tem a mesma regra com outro nome: NEXT_PUBLIC_. A documentação dele descreve o valor como inlined, uma string fixa escrita no bundle do navegador na hora do build. O Expo usa EXPO_PUBLIC_ e avisa com as mesmas palavras. Projetos antigos do Create React App usam REACT_APP_. Cada prefixo diz a mesma coisa à sua ferramenta de build: este vai na caixa.

Por que um arquivo .env parece privado e não é

Porque o hábito de guardar uma chave em um arquivo .env vem dos servidores, onde funciona.

Em um servidor, o arquivo e o código que o lê ficam em uma máquina que você controla. Um visitante recebe uma resposta dessa máquina e nunca vê o arquivo. Tirar a chave do código e colocá-la em um arquivo que o servidor lê ao iniciar é boa prática lá, e foi lá que todo tutorial que diz «coloque no .env» foi escrito.

O seu arquivo .env tem dois leitores, e os tutoriais falam de um deles. O primeiro é qualquer um que consiga ver o seu projeto: um colaborador, um repositório público no GitHub, a visualização de arquivos do próprio builder. Uma entrada no .gitignore, que é uma lista de arquivos que o git deixa de fora, mantém o .env longe desse leitor. O segundo leitor é o build, que abre o arquivo toda vez que você publica e copia tudo o que carrega o prefixo. O .gitignore não diz nada a ele.

Um arquivo, dois leitores. A parede do .gitignore detém um deles. O build leva cada valor com prefixo até o outro.

Então «meu .env está no gitignore» é verdade, e responde a outra pergunta. O arquivo ficou fora do seu repositório. Os valores com prefixo entraram no app mesmo assim, porque essa é a rota que o prefixo abre, e em um projeto do Lovable, do Bolt ou do v0 a maior parte do código que você vem editando roda no navegador, onde não há servidor atrás do qual o arquivo possa ficar.

Por que a IA recorreu ao prefixo

Porque é assim que se faz um valor funcionar em código do navegador, e o modelo não faz ideia de quanto esse valor custa.

Você pediu um mapa, ou um chat que responde perguntas sobre o seu produto. O código que o builder escreveu para isso roda no navegador do visitante, e código de navegador que lê process.env.OPENAI_API_KEY não recebe nada. O jeito de fazer o valor chegar é o prefixo. O builder renomeia a variável para VITE_OPENAI_API_KEY, a função funciona na pré-visualização, e nenhum erro aparece em lugar nenhum, porque do lado da ferramenta de build nada deu errado.

O prefixo não julga. É a mesma instrução para uma chave do Google Maps, que deve ser pública depois de restringida, e para uma chave secreta do Stripe, que pode reembolsar cada cliente que você tem. Uma pessoa que soubesse que a segunda cobra o seu cartão pararia. O modelo sabe que a função não funcionava até a linha estar lá, e depois funcionou.

É por isso que isso aparece em apps cujos donos fizeram tudo o que mandaram. O valor foi tirado do código, guardado no .env, o arquivo estava no gitignore, e o app o entrega mesmo assim, porque a única etapa que o publica parece a etapa que o faz funcionar.

Quais valores vão atrás do prefixo

Um endereço e uma chave publicável. Qualquer coisa que gaste dinheiro ou ignore as regras do seu banco de dados, não.

ValorAtrás de VITE_ ou NEXT_PUBLIC_?Por quê
VITE_SUPABASE_URLFica aquiUm endereço. Diz com qual projeto o seu app conversa e nada mais.
VITE_SUPABASE_PUBLISHABLE_KEY, ou VITE_SUPABASE_ANON_KEY em um projeto antigoFica aquiDesenhada para o navegador. Cada pedido que ela faz continua filtrado pelo Row Level Security, as regras de cada tabela que decidem linha a linha quem pode ler o quê.
Uma chave pk_live_ do StripeFica aquiMonta formulários de pagamento. Não consegue cobrar, reembolsar nem ler clientes.
Uma chave do Google MapsFica aqui, depois de restringidaPública por desenho. Uma restrição por referrer no Google Cloud é o que impede um estranho de gerar cobranças para você com ela.
Uma chave da OpenAI ou da AnthropicNuncaNão existe variante publicável. Quem a tiver gasta o seu dinheiro.
Uma chave service_role ou sb_secret_ do SupabaseNuncaIgnora o Row Level Security e lê cada linha de cada tabela.
Uma chave sk_live_ do StripeNuncaCobranças, reembolsos, repasses e cada registro de cliente.
Uma chave de acesso da AWSNuncaO que quer que essa conta consiga fazer, de qualquer lugar.

Se o seu app tem VITE_SUPABASE_URL e uma chave publicável ao lado, esse é o par que o Supabase planejou para um navegador, e o nosso escaneamento o marca como no lugar certo. O endereço e a chave publicável são o motivo de o prefixo existir.

O teste para todo o resto é se você se importaria de ver o valor impresso na sua página inicial. O prefixo o deixa um clique mais longe do que isso, em um arquivo em vez de na página, e quem quiser o arquivo o tem. Distinguir as duas famílias, pelo prefixo e, em uma chave antiga do Supabase, pelo papel dentro dela, é um artigo à parte.

O que 30.998 apps entregavam atrás do prefixo

Principalmente chaves de API do Google. 52 apps entregavam uma chave que gasta dinheiro ou lê tudo.

Em agosto de 2026 rodamos as mesmas nove verificações em 30.998 apps vibe-coded no ar. 1.332 delas, 4%, entregavam algo com cara de chave no código que cada visitante baixa. 1.142 dessas eram chaves de API do Google, que normalmente precisam de uma restrição definida no Google Cloud e não de rotação. 204 entregavam um valor de aparência aleatória ao lado de um nome como secret ou password, que pode ser uma credencial real ou não.

As caras eram raras. 33 apps entregavam uma chave da OpenAI, 9 uma chave de acesso da AWS, 5 uma chave da Anthropic, 3 uma chave secreta do Stripe e 3 uma service_role do Supabase: 52 apps ao todo, já que uma delas carregava duas. Então o valor atrás do prefixo normalmente é uma chave do Google, e a solução para isso é uma configuração. O caso raro é onde está o dano, e o que custa uma chave da OpenAI vazada é o artigo para esse.

Como verificar o que o seu próprio app entrega

Abra o app no ar, aperte F12 e busque o valor em cada arquivo carregado.

  1. Abra o seu app publicado em um navegador, no endereço real dele. A pré-visualização do builder é outro build e pode estar uma versão atrás.
  2. Aperte F12 para abrir as ferramentas de desenvolvedor e escolha a aba Sources.
  3. Aperte Ctrl+Shift+F, ou Cmd+Option+F em um Mac. Isso abre uma busca em cada arquivo que a página carregou.
  4. Cole os primeiros dez caracteres, mais ou menos, do valor que preocupa você, e não o cole em nenhum outro lugar.

Um resultado significa que o valor está na caixa que cada visitante recebe. Busque o valor e não o nome: o build normalmente troca import.meta.env.VITE_OPENAI_API_KEY pelo valor em si, então uma busca por VITE_ pode voltar vazia enquanto cada valor que havia atrás está lá.

O build troca o nome pelo valor. Buscar VITE_ no app no ar não encontra nada; buscar o valor encontra.

Se você prefere não percorrer o seu app valor por valor, o nosso escaneamento gratuito lê o seu site no ar de fora, todas as nove verificações, em uns 20 segundos e sem conta. Ele nomeia cada chave que encontra pelo tipo, diz quais ficam bem em um navegador, e escreve «Não foi possível verificar» para tudo o que não conseguiu responder em vez de um tique: escaneie o seu app.

Para onde vai um segredo de verdade

Para uma máquina da qual os seus visitantes nunca baixam nada. Em um projeto do Supabase, é uma Edge Function, um pedacinho de código de servidor que o Supabase roda para você; em um app Next.js, é uma rota de servidor; em um app do Replit, é a metade do servidor.

O formato é o mesmo em todo lugar. O seu código do navegador pede à sua função que faça o trabalho. A função guarda a chave, faz a chamada à OpenAI ou ao Stripe e devolve a resposta. A chave fica na máquina e o visitante recebe um resultado. O artigo sobre a OpenAI desenha isso como um objeto se movendo uma caixa para a direita, e a mudança é só isso.

Duas coisas dizem que o builder fez o que você pediu. A variável perdeu o prefixo, então é OPENAI_API_KEY, e vive nos secrets da própria função, definidos no painel do Supabase em Edge Functions, sem nada no .env do app. E o arquivo que a lê fica em supabase/functions/ ou app/api/, em algum lugar que o build nunca empacota, em vez de em src/.

Uma ordem importa, e é fácil invertê-la. Se uma chave secreta já foi entregue atrás de um prefixo, movê-la não faz nada pelas cópias já baixadas. Rotacione primeiro no provedor, depois mova o trabalho. Se rotacionar primeiro ou fechar o vazamento primeiro depende de a cópia já ser pública, e uma vez que ela esteve em um bundle publicado, é.

Em um projeto do Replit a mesma separação tem nome próprio, Secrets, e o que a ferramenta Secrets cobre e o que não cobre é um artigo à parte.

O que fazer agora

O que fazer

  • Busque no seu projeto VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_ e REACT_APP_. Cada ocorrência é um valor que o seu build publica de propósito. Decida para cada um se pode ser público.
  • Mantenha o endereço e a chave publicável. VITE_SUPABASE_URL e VITE_SUPABASE_PUBLISHABLE_KEY, ou a chave anon em um projeto antigo, são o par para o qual o prefixo existe.
  • Uma chave secreta atrás de um prefixo é rotacionada primeiro no provedor, depois movida. Apagar a linha não recolhe uma cópia que já foi baixada.
  • Mova o trabalho que precisava da chave para uma Edge Function ou uma rota de servidor, com a chave nos secrets dessa função e sem prefixo no nome.
  • Restrinja uma chave do Google por referrer no Google Cloud. Essa precisa de uma configuração e mantém o valor.
  • Depois da próxima publicação, busque no app no ar cada valor que você moveu.

Cada publicação empacota uma caixa nova. A próxima função que você pedir é mais uma chance de um valor receber o prefixo, e nada entre o builder e a internet lê o bundle no caminho de saída. Um escaneamento do mês passado leu o bundle do mês passado.

O Reeve Monitor lê o bundle por você. Ele roda de novo todas as nove verificações a cada hora em até três apps, avisa você quando um resultado muda, vigia a disponibilidade a cada 60 segundos e envia um relatório mensal. Uma chave que chega ao bundle com a publicação de terça aparece no reescaneamento daquela hora, quer você tenha lembrado de olhar ou não. Custa $12 por mês no preço de lista, com sete dias grátis antes de cobrar; a página de preços às vezes fica abaixo do valor daqui e nunca acima.

Se você prefere seguir isso como uma lista, a checklist de segurança de 10 minutos cobre isto e as outras coisas que vale a pena desligar em um app recém-lançado.

Perguntas frequentes

Arquivos .env são secretos?

Para o seu repositório, sim, se o arquivo estiver no .gitignore. Para os seus visitantes, não. O build lê o .env toda vez que você publica e copia cada valor com o prefixo VITE_ ou NEXT_PUBLIC_ para o JavaScript que o seu app entrega. O arquivo em si nunca sai da sua máquina; os valores que você etiquetou para o navegador, sim.

É seguro expor VITE_SUPABASE_ANON_KEY?

Ela foi feita para estar lá. A chave anon, chamada de chave publicável em um projeto novo, foi desenhada para viver em um navegador. Ela só diz a que projeto um pedido pertence, e cada pedido que ela faz passa pelo filtro das suas regras de Row Level Security. Isso vale exatamente enquanto essas regras estiverem ligadas e corretas, o que é outra coisa a verificar.

NEXT_PUBLIC_ é diferente de VITE_?

Mesma regra, outra ferramenta de build. O Next.js escreve o valor de qualquer variável NEXT_PUBLIC_ no bundle do navegador como uma string fixa na hora do build, e uma variável sem o prefixo chega vazia ao código do navegador. O Expo faz o mesmo com EXPO_PUBLIC_, e projetos antigos do Create React App com REACT_APP_. Seja qual for a ferramenta que construiu o seu app, o prefixo significa publicar.

Como verifico o que está no meu bundle?

Abra o app no ar, aperte F12, escolha Sources e aperte Ctrl+Shift+F (Cmd+Option+F em um Mac) para buscar em cada arquivo que a página carregou. Cole os primeiros caracteres do valor. Busque o valor e não o nome da variável, porque o build normalmente troca o nome pelo valor, então VITE_ pode não aparecer enquanto a chave está lá. Nosso escaneamento gratuito faz a mesma leitura de fora em uns 20 segundos.

Onde uma chave secreta deveria viver em um app do Lovable ou do Bolt?

Em uma Supabase Edge Function, com a chave definida nos secrets dessa função no painel do Supabase e sem o prefixo VITE_ no nome. O seu código do navegador chama a função, a função chama o provedor com a chave, e a chave nunca chega a um visitante. Se a chave já foi entregue, rotacione-a no provedor antes de movê-la.

O .gitignore protege as minhas chaves?

Ele mantém o arquivo .env fora do git, então ninguém que leia o seu repositório o vê. Não tem efeito nenhum sobre o build, que lê o arquivo diretamente e publica cada valor com prefixo. Um .env ignorado pelo git com VITE_OPENAI_API_KEY dentro continua entregando essa chave a cada visitante.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Todos os artigos

Não sabe como está o seu próprio app?

Faça um scan gratuito e receba uma nota clara de A a F em cerca de 20 segundos. Sem conta e sem cartão.

Verificar meu app grátis

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.