Pular para o conteúdo

Chaves da OpenAI ou da Stripe no seu app feito com IA? Leve-as para uma função no servidor

Qualquer pessoa copia uma chave do frontend. Quais podem ficar no navegador, como achar as que não podem e como levá-las para uma Edge Function do Supabase.

4 min de leitura

Você pediu à sua ferramenta de IA para colocar um chatbot, pagamentos ou envio de e-mail no app. Funcionou de primeira, e muitas vezes funcionou tão rápido porque a chave da API foi direto para o código do frontend.

Tudo o que está no frontend é enviado ao navegador de cada visitante. Se uma chave está ali, qualquer pessoa pode abrir as ferramentas de desenvolvedor, copiá-la e usá-la, na sua conta e na sua fatura. Existem scanners automáticos procurando exatamente essas chaves em sites públicos.

Quais chaves podem ficar no navegador

Nem toda chave é secreta. Muitos serviços emitem dois tipos de propósito:

Pode ficar no navegador Nunca no navegador
Chave publicável da Stripe (pk_live_…) Chave secreta ou restrita da Stripe (sk_live_…, rk_live_…)
Chave publicável ou anon do Supabase Chave secreta ou service role do Supabase (o que fazer se ela vazou)
IDs de analytics e de tags Chaves da OpenAI, da Anthropic e de outras IAs (sk-…)
Chaves de mapas restritas ao seu domínio Chaves de envio de e-mail (Resend re_…, SendGrid, Postmark)

A regra: se a chave permite gastar dinheiro, ler dados privados ou enviar algo em seu nome, ela pertence a um servidor.

O custo de uma chave vazada depende do serviço. Uma chave de IA pode gerar uma conta alta em poucas horas. Uma chave secreta da Stripe lê os seus clientes e emite reembolsos. Uma chave de e-mail permite mandar phishing a partir do seu domínio, o que ainda prejudica a entrega dos seus e-mails por meses.

Encontre as chaves no app publicado

Abra o app, aperte F12, vá em Sources (Chrome) ou Debugger (Firefox) e busque em todos os arquivos (Ctrl+Shift+F ou Cmd+Option+F) por: sk-, sk_live, rk_live, re_, sb_secret_, service_role.

Se você tem o código localmente, gere o build e busque no resultado, que é exatamente o que os visitantes recebem:

Procurar chaves secretas no build de produção
npm run build
grep -rEn "sk-[A-Za-z0-9_-]{20,}|sk_live_|rk_live_|re_[A-Za-z0-9_]{20,}|sb_secret_|service_role" dist/

Uma origem comum é o arquivo de ambiente. Em projetos Vite, o que inclui a maioria dos apps do Lovable e do Bolt, toda variável que começa com VITE_ é copiada para o código do navegador. No Next.js, vale o mesmo para NEXT_PUBLIC_. Um segredo em VITE_OPENAI_API_KEY é público, mesmo morando no .env.

Corrigindo

1. Troque a chave primeiro

Mudar o código não “desvaza” a chave. No painel do provedor, crie uma chave nova e apague a antiga. Faça isso primeiro, mesmo que você ache que ninguém a viu.

Aproveite e defina um limite de gasto mensal nos provedores de IA. Ele transforma um possível desastre num prejuízo com teto.

2. Guarde a chave nova como segredo do servidor

No Supabase, as Edge Functions leem segredos do ambiente. Adicione a chave no painel, em Edge Functions → Secrets, ou pela CLI:

Guardar a chave onde só o código de servidor a lê
supabase secrets set OPENAI_API_KEY=sk-your-new-key

O Lovable tem um lugar próprio para segredos e os pede quando uma integração precisa de um, em vez de colocá-los no código. Na Vercel ou na Netlify, use variáveis de ambiente sem o prefixo VITE_ ou NEXT_PUBLIC_, lidas só por funções de servidor.

3. Chame o serviço a partir de uma função no servidor

O navegador chama a sua função; a sua função chama o serviço com a chave secreta. Uma Edge Function mínima para um recurso de IA:

supabase/functions/ask/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2';

Deno.serve(async (req) => {
  // 1. Only signed-in users may call this function.
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')! // public key: it only identifies the project
  );
  const token = (req.headers.get('Authorization') ?? '').replace('Bearer ', '');
  const { data: { user } } = await supabase.auth.getUser(token);
  if (!user) return new Response('Unauthorized', { status: 401 });

  // 2. Accept only what the feature needs.
  const { question } = await req.json();
  if (typeof question !== 'string' || question.length > 2000) {
    return new Response('Bad request', { status: 400 });
  }

  // 3. The secret key never leaves the server.
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: Deno.env.get('OPENAI_MODEL'),
      max_tokens: 500,
      messages: [{ role: 'user', content: question }]
    })
  });

  const data = await response.json();
  return Response.json({ answer: data.choices?.[0]?.message?.content ?? '' });
});

No app, a chamada passa a ser supabase.functions.invoke('ask', { body: { question } }), e o cliente do Supabase envia junto o token do usuário logado.

4. Pagamentos: deixe o servidor decidir o preço

Na Stripe, mudar a chave de lugar não basta. Se o navegador diz à sua função quanto cobrar, alguém pode trocar o valor. O navegador deve mandar só o que está sendo comprado (o ID de um produto ou plano); o servidor busca o preço, cria a Checkout Session com a chave secreta e devolve o link. Depois, confirme os pagamentos por webhooks verificados com a assinatura da Stripe, nunca por uma página de “sucesso” que o usuário pode abrir direto.

Antes de cada publicação

  • Procure os padrões acima no build.
  • Mantenha segredos fora de qualquer variável com prefixo público.
  • Quando a IA sugerir colocar uma chave no frontend “por enquanto”, peça uma função no servidor.

Se o seu app lida com pagamentos, recursos de IA ou dados pessoais, o nosso Security Check gratuito procura chaves expostas, funções abertas e webhooks sem assinatura, de fora e sem alterar nada, e envia por escrito os problemas mais graves.

Chaves de APIApps feitos com IASupabaseBolt

Este artigo também está disponível em: EnglishEspañol日本語中文

Todos os artigos