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.

Cómo ocultar una clave API en JavaScript del frontend sin filtrarla
Cristian Da Conceicao
Fundador de Picasso IA

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

Desarrollador inspeccionando la pestaña de red del navegador en un monitor grande en una oficina luminosa

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.

FrameworkPrefijo que se hace públicoQué pasa
ViteVITE_El valor se incrusta en el paquete
Next.jsNEXT_PUBLIC_El valor se incrusta en el código del cliente
Create React AppREACT_APP_El valor se incrusta durante la compilación
NuxtNUXT_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

Desarrollador preocupado apoyando una mano en la frente al atardecer frente a un equipo portátil

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 filtradaAbuso típicoQué te cuesta
Token de LLMChatbots gratuitos, generación de spamFactura de tokens, bloqueo por límite de uso
Token de generación de imágenes o videoRenders masivos, reventaFactura de uso de GPU
Token de mapas o búsquedaScraping a gran escalaAgotamiento de la cuota
Secreto de base de datos o almacenamientoLectura o borrado de registrosFiltració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

Vista cenital de una mano dibujando un diagrama de arquitectura de tres cajas en una libreta

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:

  1. El secreto vive solo en variables de entorno del servidor. Nunca en el repositorio, nunca en una variable al estilo NEXT_PUBLIC_.
  2. 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.
  3. 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

Vista amplia desde abajo de un pasillo con racks de servidores en un centro de datos

¿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.

// worker.js
const UPSTREAM =
  "https://api.picassoia.com/v1/models/picassoia/picassoia-image/predictions";

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (request.method !== "POST" || url.pathname !== "/api/generate") {
      return new Response("Not found", { status: 404 });
    }

    const { prompt } = await request.json().catch(() => ({}));
    if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 500) {
      return new Response("Bad request", { status: 400 });
    }

    const upstream = await fetch(UPSTREAM, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.PICASSOIA_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ input: { prompt } }),
    });

    const data = await upstream.json();
    return Response.json(
      { id: data.id, status: data.status },
      { status: upstream.status }
    );
  },
};

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

Torno de acero inoxidable en el vestíbulo de una oficina moderna con un visitante que lo atraviesa

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 endpointLímite inicialMotivo
Generación de texto20 solicitudes por minuto por usuarioBarata por llamada y fácil de abusar con ella
Generación de imágenes5 solicitudes por minuto por usuarioCosto real de GPU en cada llamada
Generación de video2 solicitudes por minuto por usuarioLenta y cara
Consultas de estado de solo lectura60 solicitudes por minuto por usuarioLas 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

Primer plano macro de una mano sujetando un anillo metálico desgastado de piezas de latón

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

Ingeniero dibujando un flujo de tres pasos con flechas en una pizarra de cristal en una sala de reuniones

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:

  1. El usuario inicia sesión en tu backend.
  2. 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.
  3. El navegador usa directamente ese token temporal para la petición pesada.
  4. 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

Dos desarrolladores trabajando uno al lado del otro en una mesa compartida con una caja fuerte de acero entre ellos

Si un token ha estado público aunque sea una hora, asume que alguien lo copió. Sigue esta lista en orden:

  1. Revoca o rota el token en el panel del proveedor ahora mismo.
  2. Despliega el reemplazo solo en las variables de entorno del servidor.
  3. Lee los registros de uso del periodo de exposición y busca IP, modelos o picos desconocidos.
  4. Reduce los límites de gasto antes de hacer cualquier otra cosa.
  5. 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:

  1. 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.
  2. Abre la página del modelo. Ve a Claude Sonnet 5 en PicassoIA.
  3. 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.
  4. 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."
  5. 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

Diseñadora sonriente en un escritorio luminoso de estudio mirando un monitor lleno de fotografías

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.

Compartir este artículo

Elige tu idioma