Vai al contenuto

Guida

La tua chiave API LLM è probabilmente nel bundle del frontend

Perché le variabili VITE_ e NEXT_PUBLIC_ finiscono nel browser, come cercare in una build una chiave OpenAI o Anthropic esposta e la route lato server che lo risolve.

· 7 min di lettura · A cura del team vetkit

Una grande chiave esposta dentro la finestra di vetro di un browser; un robot lucida il vetro senza accorgersi di nulla, mentre uno sviluppatore indica allarmato e una mano guantata si allunga verso la chiave.Una grande chiave esposta dentro la finestra di vetro di un browser; un robot lucida il vetro senza accorgersi di nulla, mentre uno sviluppatore indica allarmato e una mano guantata si allunga verso la chiave.

Chiedi a un assistente di aggiungere una chat alla tua app React. Installa l’SDK di OpenAI, legge la chiave da import.meta.env.VITE_OPENAI_API_KEY e la funzionalità va al primo colpo. Il problema è che va anche per chiunque apra gli strumenti per sviluppatori del browser, trovi la chiave nel tuo JavaScript e cominci a inviare richieste a tuo nome.

È uno degli errori più comuni che vediamo nei progetti sviluppati con l’aiuto dell’AI, ed è facile che sfugga in review perché non sembra esserci nulla di sbagliato. La chiave non è hard-coded. Sta in un file .env correttamente ignorato da git. Il codice la legge da una variabile d’ambiente, che è esattamente ciò che tutti consigliano di fare.

Come succede

Gli strumenti di build del frontend non hanno variabili d’ambiente a runtime. Il browser esegue un bundle statico. Quindi, quando il tuo codice legge import.meta.env.VITE_SOMETHING o process.env.NEXT_PUBLIC_SOMETHING, il bundler sostituisce quell’espressione con il valore letterale durante la build. Dopo la build la variabile non esiste più: resta solo una stringa in un file JavaScript che ogni visitatore scarica.

I framework sono chiari su questo punto. Next.js dice che le variabili pubbliche vengono inserite inline in tutto il JavaScript inviato al browser. Vite espone al codice client solo le variabili con il prefisso VITE_ proprio per evitare che le altre trapelino per errore. Il prefisso è il consenso esplicito. Quando un assistente sceglie VITE_OPENAI_API_KEY perché il nome senza prefisso non funzionava nel browser, ha dato quel consenso al posto tuo.

Esiste anche una variante più subdola. Alcuni template iniziali copiano la chiave nel bundle con l’opzione define di Vite, per esempio 'process.env.GEMINI_API_KEY': JSON.stringify(...) in vite.config.ts. La variabile non ha un prefisso pubblico, quindi sembra sicura, ma l’effetto è esattamente lo stesso.

Gli SDK ufficiali cercano di impedirlo. L’SDK Node di OpenAI si rifiuta di girare in un browser a meno che tu non passi dangerouslyAllowBrowser: true, e il suo README spiega perché: espone le tue credenziali segrete nel codice lato client. A volte gli assistenti aggiungono quel flag per far sparire un errore. Altri client, come chiamate HTTP generiche o SDK senza un controllo sul browser, non ti avvisano affatto.

Quali variabili sono pubbliche

Se una variabile inizia con uno di questi prefissi, dai per scontato che il suo valore sia leggibile da chiunque carichi la tua app:

  • VITE_ in Vite (e nei framework basati su di esso)
  • NEXT_PUBLIC_ in Next.js
  • REACT_APP_ in Create React App
  • EXPO_PUBLIC_ in Expo e nelle app React Native costruite con Expo
  • VUE_APP_ in Vue CLI e GATSBY_ in Gatsby
  • PUBLIC_ in Astro e nei moduli env pubblici di SvelteKit

Questi prefissi vanno bene per valori pensati per essere pubblici, come l’ID di un sito di analytics o l’URL della tua API. Non vanno mai bene per il segreto di un provider: OpenAI, Anthropic, Google Gemini, Mistral, una chiave segreta di Stripe, una service role key di un database.

Controlla la tua build

Il test più rapido è fare la build dell’app e cercare nell’output il prefisso della chiave. Le chiavi di progetto OpenAI iniziano con sk-proj-, quelle Anthropic con sk-ant- e le chiavi API di Google con AIza.

bash
npm run build# Vite writes to dist/, Next.js static chunks live in .next/static/grep -rEo "sk-(proj|ant)-[A-Za-z0-9_-]{8}|AIza[0-9A-Za-z_-]{8}" dist .next/static 2>/dev/null

Se il comando stampa qualcosa, la chiave è nel tuo bundle. Controlla un sito in produzione allo stesso modo: apri gli strumenti per sviluppatori, vai nella scheda Sources o Network e cerca negli script caricati. Se riesci a trovarla in trenta secondi, ci riesce anche uno script che scandaglia i siti cercando proprio questo.

La soluzione

Sposta la chiamata al provider su un server che controlli e fai in modo che il browser parli con quel server. In Next.js è un route handler. In una single-page app con Vite può essere una funzione serverless sullo stesso host. La chiave mantiene un nome senza prefisso, così il bundler non la inserisce mai inline.

app/api/chat/route.ts
import OpenAI from 'openai'; // Server-only: no NEXT_PUBLIC_ prefix, never shipped to the browser.const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export async function POST(request: Request) {  const user = await requireSignedInUser(request); // your auth check  await rateLimit(user.id); // your limiter: per user, per minute   const { message } = await request.json();  if (typeof message !== 'string' || message.length > 4000) {    return Response.json({ error: 'Invalid message' }, { status: 400 });  }   const response = await client.responses.create({    model: 'gpt-5-mini',    input: message,    max_output_tokens: 800,  });  return Response.json({ reply: response.output_text });}

I tre dettagli attorno alla chiamata contano quanto spostarla. Senza un controllo di autenticazione, la tua route diventa un proxy gratuito e anonimo verso il modello: la stessa falla, con qualche passaggio in più. Senza un rate limit, un singolo utente autenticato può comunque far lievitare la bolletta. Senza un limite sull’output, una singola richiesta può generare molti più token di quanti ne servano alla tua funzionalità.

Tieni anche il nome del modello e i limiti sul server

Se è il browser a inviare il nome del modello o max_output_tokens, un visitatore può modificarli. Decidi questi valori lato server e accetta dal client solo il messaggio dell’utente.

Se la chiave è già stata rilasciata

Rimuovere la variabile dal codice da solo non basta. Le vecchie build restano in cache nei browser e nelle CDN, e la chiave potrebbe essere già stata copiata. Considerala compromessa:

  1. Crea una nuova chiave nella dashboard del provider e revoca quella vecchia.
  2. Inserisci la nuova chiave solo nelle impostazioni d’ambiente lato server, con un nome senza prefisso pubblico.
  3. Controlla nella pagina di utilizzo del provider se c’è traffico che non riconosci e imposta un limite di spesa.
  4. Rifai il deploy, poi ripeti la ricerca nella build vista sopra per confermare che il nuovo bundle sia pulito.

Se la chiave è mai stata committata in git, ruotala anche se hai cancellato il file. La cronologia di git conserva il vecchio valore.

Intercettarla prima del rilascio

Questo errore è facile da eliminare con l’automazione. Un secret scanner intercetta le chiavi committate nel repository. Non può però vedere una chiave che sta in un file .env ignorato, quindi ti serve anche un controllo che legga il codice e si accorga di una variabile pubblica passata a un client LLM.

Questo secondo schema è una delle regole di sicurezza per LLM e agent nelle scansioni complete di vetkit: segnala un valore proveniente da una variabile d’ambiente lato client (per esempio import.meta.env.VITE_* o process.env.NEXT_PUBLIC_*) che arriva a un client SDK di un LLM nel codice del browser, e spiega come spostare la chiamata lato server. Il Security Quick Scan gratuito di vetkit copre segreti committati, dipendenze con vulnerabilità note e postura di sicurezza di base. Nessuno dei due sostituisce una rapida occhiata all’output della tua build, che è la prova più diretta che puoi ottenere.

Richiedi l’accesso anticipato

Raccontaci qualcosa del tuo team e dei repository che vuoi sottoporre ad audit. Accogliamo i team a piccoli gruppi e ti risponderemo via email.

Usiamo questi dati solo per rispondere alla tua richiesta. Informativa sulla privacy