Cómo ocultar una clave API en JavaScript del frontend sin filtrarla
El código del navegador es público por diseño, así que cualquier clave API que incluyas en un paquete de React, Vue o JavaScript puro se puede copiar en segundos. Este artículo muestra cómo un proxy pequeño en el servidor, los límites de uso, los tokens restringidos y la rotación rápida mantienen tus credenciales fuera del alcance de cualquiera.
Abre cualquier sitio que hayas creado el mes pasado, pulsa F12 y haz clic en la pestaña Network. Todas las cabeceras que envió tu JavaScript están ahí en texto plano, incluido el valor Authorization que estabas seguro de que nadie encontraría. Esa es la incómoda verdad detrás de cómo ocultar una clave API en JavaScript del frontend: ahí no se puede ocultar. Lo que sí puedes hacer es dejar de enviar el secreto al navegador y dejar que un servidor que controlas haga la llamada en nombre de tus usuarios.
Este artículo muestra exactamente cómo funciona. Verás por qué los empaquetadores filtran las variables de entorno, cómo los bots encuentran tokens en cuestión de minutos tras un despliegue y cómo construir un proxy pequeño en Express o en Cloudflare Workers que mantenga la credencial en el servidor. Después añadimos límites de uso, validación de entradas, tokens restringidos y un plan de respuesta claro para el día en que algo se filtre igualmente.
💡 Respuesta corta: si un secreto va en código que se ejecuta en el navegador, es público. Ocúltalo moviendo la petición a un backend, no codificando, dividiendo o desordenando la cadena.
Por qué el código del frontend no puede guardar secretos
Un navegador funciona descargando tu código y ejecutándolo en el equipo del visitante. Todo lo que descarga, el visitante puede leerlo: HTML, CSS, paquetes de JavaScript, mapas de código fuente y cada petición que hace tu código. Ningún ajuste, marca o paso de compilación vuelve invisible una cadena para la persona en cuyo equipo se está ejecutando.
Todo es legible en el navegador
Tres lugares exponen un token sin necesidad de ninguna habilidad de hacking:
La pestaña Network. Cada petición muestra su URL, sus cabeceras y su carga útil. Un token Bearer en una cabecera está a un clic.
La pestaña Sources. Tu paquete está ahí mismo y, si los mapas de código fuente están activados, tus archivos originales también, con los comentarios incluidos.
Ver código fuente y curl. Cualquiera puede descargar tu paquete y ejecutar grep para buscar prefijos de tokens como sk_ o pia_sk_.
Los empaquetadores incrustan tus variables
Un error habitual es este: "lo puse en un archivo .env, así que es privado". El archivo .env es privado. Lo que hace tu empaquetador con él es otra historia. Vite, Next.js y Create React App sustituyen las variables con prefijo especial por sus valores literales durante la compilación.
Framework
Prefijo que se hace público
Qué pasa
Vite
VITE_
El valor se incrusta en el paquete
Next.js
NEXT_PUBLIC_
El valor se incrusta en el código del cliente
Create React App
REACT_APP_
El valor se incrusta durante la compilación
Nuxt
NUXT_PUBLIC_
El valor acaba en la configuración de runtime pública
Así que VITE_PROVIDER_TOKEN=abc123 en un archivo .env acaba como la cadena literal "abc123" dentro de assets/index-xxxx.js. Las variables sin el prefijo público quedan fuera del paquete del cliente, y por eso el secreto debe estar en el lado del servidor.
La ofuscación solo te hace perder tiempo
Base64, la división de cadenas, los caracteres invertidos, los trucos con XOR: nada de eso funciona, porque tu código tiene que reconstruir el valor real antes de enviar la petición. Una vez que la petición sale, la pestaña Network muestra el resultado final. La ofuscación le da al atacante diez minutos de leve molestia y a ti te da un dolor de cabeza de mantenimiento permanente.
Cómo se producen las filtraciones en realidad
Los bots escanean repositorios y paquetes
El método más rápido es también el más aburrido: abrir la página, activar la función y leer la cabecera. No hacen falta scripts. Los escáneres automatizados van más allá: rastrean repositorios públicos, paquetes de npm y sitios en producción buscando formatos de token conocidos, y muchos proveedores usan prefijos reconocibles (sk_, ghp_, pia_sk_) precisamente para que los escáneres los detecten.
Un token subido a un repositorio público de GitHub puede ser recogido en cuestión de minutos. Algunos proveedores lo escanean y lo revocan automáticamente, lo cual es un extra agradable, pero no es un plan.
Lo que cuesta realmente una filtración
En las APIs de pago por uso, la factura es el daño visible. El daño oculto es peor: cuotas agotadas que dejan tu propia aplicación fuera de servicio, abuso señalado en tu cuenta y, con permisos laxos, acceso a datos reales.
Credencial filtrada
Abuso típico
Qué te cuesta
Token de LLM
Chatbots gratuitos, generación de spam
Factura de tokens, bloqueo por límite de uso
Token de generación de imágenes o video
Renders masivos, reventa
Factura de uso de GPU
Token de mapas o búsqueda
Scraping a gran escala
Agotamiento de la cuota
Secreto de base de datos o almacenamiento
Lectura o borrado de registros
Filtración de datos
Las aplicaciones de IA son el objetivo favorito. Modelos como GPT 5.6 Luna o Claude Sonnet 5 cobran por token, así que una credencial robada se convierte directamente en cómputo gratuito para otro, a tu costa.
Pon un proxy entre el navegador y la API
La solución está en la arquitectura. En lugar de que el navegador llame directamente al proveedor, el navegador llama a tu servidor, y tu servidor llama al proveedor.
Browser -> POST /api/generate -> Your server -> Provider API
(holds the secret)
Tres reglas mantienen honesto este diseño:
El secreto vive solo en variables de entorno del servidor. Nunca en el repositorio, nunca en una variable al estilo NEXT_PUBLIC_.
El navegador envía solo la entrada del usuario. Un prompt, un ID, una opción de una lista. Nunca una URL, una cabecera ni un nombre de modelo que elija libremente.
El servidor decide qué está permitido. Valida la entrada, adjunta la credencial, reenvía la llamada y devuelve solo los campos que la página necesita.
Un proxy de Express que funciona
Este ejemplo reenvía una petición de imagen a la API de PicassoIA, que se autentica con un token Bearer en la cabecera Authorization y expone las predicciones en /v1/models/{owner}/{name}/predictions. La misma estructura sirve para cualquier otro proveedor.
// server.js
import express from "express";
import rateLimit from "express-rate-limit";
const app = express();
app.use(express.json({ limit: "20kb" }));
app.use("/api/", rateLimit({ windowMs: 60_000, limit: 10 }));
const UPSTREAM =
"https://api.picassoia.com/v1/models/picassoia/picassoia-image/predictions";
app.post("/api/generate", async (req, res) => {
const { prompt } = req.body ?? {};
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 500) {
return res.status(400).json({ error: "Prompt must be 1 to 500 characters." });
}
try {
const upstream = await fetch(UPSTREAM, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PICASSOIA_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ input: { prompt } }),
});
const data = await upstream.json();
// Return only what the browser needs, never the raw upstream body.
res.status(upstream.status).json({ id: data.id, status: data.status });
} catch {
res.status(502).json({ error: "Upstream request failed." });
}
});
app.listen(3000);
Arráncalo con node --env-file=.env server.js (Node 20.6 o más reciente) para que el token venga de un archivo sin seguimiento en Git. La API de PicassoIA es asíncrona: creas una predicción y luego la consultas repetidamente. Añade una segunda ruta GET /api/result/:id construida de la misma forma y comprueba los campos exactos de la respuesta en la página de la API de PicassoIA.
Versión sin servidor en Cloudflare Workers
¿No quieres mantener un servidor? Un Worker hace el mismo trabajo en menos líneas. Guarda el secreto con npx wrangler secret put PICASSOIA_TOKEN y nunca tocará tu repositorio.
Vercel Functions, Netlify Functions y AWS Lambda siguen el mismo patrón: una ruta pequeña, un secreto en la configuración de la plataforma y ninguna credencial en el código del cliente.
El código del frontend tras la corrección
El navegador ahora solo habla con tu propia ruta:
async function generate(prompt) {
const res = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
}
Compila la aplicación, abre el paquete y busca en él. No debería quedar nada que encontrar.
Blinda también el proxy
Un proxy sin límites es solo una forma más cómoda de que desconocidos gasten tu dinero. Trátalo como un endpoint público, porque lo es.
💡 Sobre CORS: configurar Access-Control-Allow-Origin con tu propio dominio evita que otros sitios web llamen a tu proxy desde el navegador de un visitante. No sirve de nada frente a curl o a un script. La protección real viene de la autenticación, los límites de uso y la validación.
Límites de uso por usuario o por IP
Limita por usuario autenticado cuando tengas inicio de sesión, y por dirección IP cuando no lo tengas. Detrás de una CDN o un balanceador de carga, asegúrate de que tu framework lea la IP real del cliente (en Express, eso significa configurar trust proxy correctamente), o todos los visitantes compartirán un mismo contador.
Tipo de endpoint
Límite inicial
Motivo
Generación de texto
20 solicitudes por minuto por usuario
Barata por llamada y fácil de abusar con ella
Generación de imágenes
5 solicitudes por minuto por usuario
Costo real de GPU en cada llamada
Generación de video
2 solicitudes por minuto por usuario
Lenta y cara
Consultas de estado de solo lectura
60 solicitudes por minuto por usuario
Las consultas repetidas son lo habitual
Trata esas cifras como puntos de partida y ajústalas según el tráfico real.
Valida cada entrada
Nunca reenvíes el cuerpo de la petición del navegador tal como llega. Revisa cada campo:
Limita la longitud del prompt. El ejemplo anterior rechaza cualquier cosa de más de 500 caracteres.
Usa una lista de modelos permitidos. Deja que la página envíe "fast" o "quality" y luego asigna esas etiquetas a nombres de modelo reales en el servidor.
Limita el tamaño de la salida. Establece un máximo de tokens de salida o de imágenes por llamada.
Rechaza los campos desconocidos. Si el esquema dice prompt, nada más pasa.
Fija límites de gasto en el proveedor
La mayoría de proveedores permiten fijar límites de gasto mensuales y alertas. Actívalos. Es la red de seguridad para el día en que fallen todas las demás capas. Usa además un token distinto por proyecto y por entorno, para que revocar uno nunca tumbe el resto.
Cuándo un token público está bien
No todas las credenciales son secretos. Algunas están pensadas para navegadores: la configuración web de Firebase, los tokens publicables de Stripe (los que empiezan por pk_) y los tokens de Google Maps JavaScript. Solo son seguros si los restringes.
Restringe por dominio y ámbito
Abre el panel del proveedor y aplica todas las restricciones que ofrezca:
Límites por referrer HTTP para que el token solo funcione desde yourdomain.com.
Límites de ámbito de la API para que un token de Maps pueda llamar a Maps y a nada más.
Cuotas diarias para que una oleada de abuso choque con un techo.
Una advertencia: las comprobaciones de referrer dependen de la cabecera Referer, y los clientes que no son navegadores pueden falsificarla. Las restricciones reducen el abuso casual, pero no convierten un token público en un secreto. Mantén el valor acotado, con ámbito limitado y con tope de presupuesto.
Tokens de corta duración para navegadores
Algunas tareas son demasiado pesadas para retransmitirlas por tu servidor, como las subidas grandes o el streaming en tiempo real. Para esas, usa un intercambio de tokens:
El usuario inicia sesión en tu backend.
Tu backend pide al proveedor un token de corta duración y ámbito limitado, o firma un JWT que caduca en 5 a 15 minutos.
El navegador usa directamente ese token temporal para la petición pesada.
El token caduca por sí solo, así que un valor copiado pierde valor enseguida.
Varios proveedores de tiempo real y de almacenamiento ofrecen tokens efímeros justo para este patrón. Tu secreto de larga duración nunca sale del servidor.
Qué hacer después de una filtración
Revoca primero, investiga después
Si un token ha estado público aunque sea una hora, asume que alguien lo copió. Sigue esta lista en orden:
Revoca o rota el token en el panel del proveedor ahora mismo.
Despliega el reemplazo solo en las variables de entorno del servidor.
Lee los registros de uso del periodo de exposición y busca IP, modelos o picos desconocidos.
Reduce los límites de gasto antes de hacer cualquier otra cosa.
Avisa a tu equipo y comprueba si ese mismo valor se reutilizó en algún otro sitio.
Limpia el historial de Git y los paquetes
Borrar el secreto de tu último commit no soluciona nada, porque el historial sigue guardándolo. Los despliegues antiguos, las cachés de la CDN y los mapas de código fuente públicos también pueden conservarlo. La rotación es la solución real. Reescribir el historial con git filter-repo es higiene para después.
Luego haz que una repetición sea poco probable:
Añade un escáner de pre-commit como gitleaks.
Activa la protección de push y el escaneo de secretos en tu host de Git.
No publiques mapas de código fuente en producción, o sírvelos solo a tu herramienta de seguimiento de errores.
Añade un paso de CI que busque los prefijos de token conocidos en el paquete compilado y haga fallar la compilación si encuentra alguno.
Audita tu paquete con un LLM
Un LLM es un segundo par de ojos rápido para este trabajo. Así se usa Claude Sonnet 5 en PicassoIA para detectar filtraciones y redactar tu proxy:
Compila y escanea primero. Ejecuta npm run build y luego grep -rE "sk_|pk_|pia_sk_|Bearer " dist/ para detectar tú mismo los casos evidentes.
Pega solo código redactado. Incluye los archivos que hacen llamadas de red, con cada valor real sustituido por REDACTED. Nunca pegues un secreto activo en ninguna herramienta de chat.
Haz una pregunta concreta. Por ejemplo: "Enumera cada lugar donde este código envía una credencial desde el navegador y reescribe cada uno para llamar a una ruta del servidor en su lugar."
Revisa la respuesta según las reglas anteriores. Comprueba la validación, los límites de uso y el manejo de errores, y luego prueba en DevTools.
💡 Consejo: para una segunda opinión, ejecuta el mismo prompt en GPT 5.6 Sol o pide una primera revisión rápida con Gemini 3.5 Flash. Distintos modelos detectan distintos errores.
Crea aplicaciones de imagen sin filtrar tokens
Todo lo anterior se aplica con más fuerza cuando tu aplicación genera imágenes o video, porque cada llamada consume tiempo de GPU. El patrón sigue siendo el mismo: la página recoge un prompt, tu proxy guarda la credencial y PicassoIA hace el render.
Elige un modelo que encaje con tu producto. Flux 2 Pro es adecuado para fotorrealismo detallado, Seedream 4.5 gestiona prompts con muchos elementos, P-Image es una opción rápida para previsualizaciones y GPT Image 2 destaca con el texto dentro de las imágenes. Para movimiento, explora los modelos de texto a video en el catálogo completo de modelos de PicassoIA.
Aquí tienes una lista de comprobación rápida antes de tu próximo despliegue:
Ninguna credencial aparece en el paquete compilado ni en los mapas de código fuente.
El navegador llama a tu proxy, nunca al proveedor.
La longitud del prompt, la elección del modelo y el tamaño de la salida se validan en el servidor.
Los límites de uso y el tope de gasto del proveedor están activos.
Cualquier token público está restringido por dominio, ámbito y cuota.
Conoces los pasos de revocación de cada token antes de necesitarlos.
¿Listo para verlo funcionar? Abre PicassoIA, genera unas cuantas imágenes con los modelos de arriba y conecta después el mismo prompt a tu propio proxy. Prueba distintos estilos y prompts, y lanza una aplicación en la que lo único que los visitantes puedan copiar del navegador sea la imagen final.