La pantalla se ve correcta, la respuesta de la IA llega y el proyecto parece listo para enseñarlo. Después abres las herramientas del navegador, buscas la pestaña Network y encuentras una cadena que nunca debió salir de tu servidor: la clave de API.

Este error aparece con frecuencia en proyectos de aprendizaje porque “guardar la clave en una variable de entorno” suena seguro. Pero una variable que termina dentro del código enviado al navegador dejó de ser privada, aunque el archivo original se llame .env. La regla práctica es más estricta: si el navegador puede leer el valor, una persona usuaria también puede inspeccionarlo.
En este artículo investigarás cómo ocurre la exposición, por qué un prefijo como VITE_ no convierte un secreto en público seguro y cómo cambiar una llamada directa a un proveedor de IA por una ruta propia. El escenario es ficticio: una panadería de Guadalajara quiere resumir comentarios de clientes en una pequeña aplicación. Los nombres, pedidos y cantidades en MXN son únicamente datos de práctica.
La pista no está en el archivo .env
Imagina que una persona principiante crea un proyecto con Vite. Para que el frontend pueda leer la clave, escribe algo parecido a esto:
# .env.local
VITE_AI_API_KEY=valor-secreto-de-pruebaLuego usa la variable en un componente:
const apiKey = import.meta.env.VITE_AI_API_KEY;
const respuesta = await fetch("https://api.proveedor.example/v1/responses", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
input: "Resume este comentario de cliente",
}),
});El archivo local puede estar ignorado por Git y aun así el secreto aparecerá en el resultado de compilación. La documentación oficial de Vite explica que las variables con prefijo VITE_ se sustituyen durante el build y quedan expuestas al código del cliente. El prefijo controla qué variable recibe el frontend; no ofrece una caja fuerte.
La documentación de Vite también recomienda no poner información sensible, como claves de API, en variables VITE_*. Cambiar el nombre a PUBLIC_AI_KEY, ocultarlo en una constante o dividirlo en varios strings no modifica el hecho importante: el navegador necesita recibirlo para utilizarlo.
La investigación empieza en DevTools
No necesitas imaginar un ataque sofisticado para comprobar el problema en tu propio proyecto. Abre la aplicación en un entorno de prueba y observa qué información recibe el navegador. En las herramientas de desarrollador, revisa Sources, busca el nombre de la variable y examina los encabezados de una solicitud en Network.
| Lo que encuentras | Qué significa | Decisión inmediata |
|---|---|---|
| La clave aparece en un bundle JavaScript | El cliente puede leerla. | Revoca la clave de prueba y mueve la llamada al servidor. |
| La clave aparece en un header de la solicitud | Se está enviando desde el navegador. | No la protejas con un cambio de nombre; elimina su recorrido por el cliente. |
| Solo existe en el servidor y no llega al response | La separación puede ser correcta. | Revisa autenticación, límites, logs y errores antes de considerarla terminada. |
| La clave aparece en un repositorio público | Debe considerarse comprometida. | Revócala, reemplázala y limpia el historial según el proveedor. |
La guía de seguridad de OpenAI recomienda no desplegar claves en browsers o aplicaciones móviles, no hacer commit de ellas y enrutar las solicitudes por un backend propio. Aunque utilices otro proveedor, el razonamiento general es el mismo: un browser no es un almacén secreto.
Qué sí puede vivir en el frontend
No toda credencial es igual. Una aplicación puede mostrar un identificador público, una URL pública, una clave diseñada expresamente para el cliente o un token de sesión de corta duración. La clasificación depende del contrato del servicio. No conviertas “está en una variable de entorno” en la única pregunta.
| Valor | ¿Puede llegar al navegador? | Qué debes confirmar |
|---|---|---|
| URL pública del backend | Sí | Que el endpoint tenga controles propios y no revele una credencial detrás. |
| Clave privada de un proveedor | No | Debe permanecer en el servidor o en un gestor de secretos. |
| Identificador de proyecto | Depende | Leer la documentación del proveedor y revisar qué permisos concede. |
| Token temporal para una operación limitada | Solo si el diseño lo contempla | Vigencia, alcance, audiencia, revocación y límites. |
| Contraseña, token de administración o secreto de firma | No | Nunca incluirlo en HTML, JavaScript público o repositorio. |
La propia existencia de un campo llamado PUBLIC no demuestra que el valor sea seguro. Lee el contrato de la API. Para complementar esta revisión, consulta la guía de Skydutz sobre verificar código generado por IA desde las amenazas, porque una respuesta que funciona todavía puede exponer permisos o datos que no necesitabas compartir. OWASP advierte en su guía de seguridad REST que una API key no debe ser el único control para recursos sensibles o de alto valor. La clave identifica una credencial; no reemplaza autorización, validación ni límites.
La arquitectura que separa la interfaz de la credencial
La alternativa introductoria tiene tres piezas:
- El navegador recibe la entrada de la persona y llama a una ruta de tu aplicación.
- Tu backend valida la entrada, consulta el proveedor usando una variable privada y decide qué respuesta devolver.
- El navegador recibe solo el resultado necesario, nunca la credencial que permitió obtenerlo.
El flujo puede representarse así:
Browser
│ POST /api/resumir-comentario
│ { comentario: "..." }
▼
Tu backend
│ valida longitud y usuario
│ lee process.env.PROVIDER_API_KEY
│ llama al proveedor
▼
Proveedor de IA
│ devuelve el resultado
▼
Tu backend
│ filtra y registra un resultado seguro
▼
BrowserUn endpoint mínimo en Node.js podría tener esta forma conceptual:
import express from "express";
const app = express();
app.use(express.json({ limit: "20kb" }));
app.post("/api/resumir-comentario", async (req, res) => {
const comentario = req.body?.comentario;
if (typeof comentario !== "string" || comentario.length < 1 || comentario.length > 2000) {
return res.status(400).json({ error: "Comentario no válido" });
}
try {
const respuesta = await llamarAlProveedor({
apiKey: process.env.PROVIDER_API_KEY,
input: comentario,
});
return res.json({ resumen: extraerResumenSeguro(respuesta) });
} catch (error) {
console.error("Fallo al resumir comentario", { nombre: error.name });
return res.status(502).json({ error: "No se pudo procesar el comentario" });
}
});El ejemplo no es una aplicación completa ni una receta para copiar en producción. Faltan autenticación, autorización, límites por usuario, validación del proveedor, observabilidad y una política de datos. Su función es hacer visible la frontera: process.env.PROVIDER_API_KEY se consulta en el servidor y nunca se interpola en el JavaScript público.
El frontend debe pedir una capacidad, no una llave
La versión del cliente cambia de forma importante:
async function resumirComentario(comentario) {
const respuesta = await fetch("/api/resumir-comentario", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ comentario }),
});
if (!respuesta.ok) {
throw new Error("No se pudo obtener el resumen");
}
return respuesta.json();
}El navegador ya no solicita “usa esta credencial para hablar con el proveedor”. Solicita una capacidad concreta: “resume este comentario”. Si quieres practicar cómo formular esa capacidad sin pedirle a la IA que decida toda la lógica, puedes revisar la guía de Skydutz sobre prompts de programación como contratos de evidencias. Esa diferencia permite que el servidor decida quién puede usarla, cuántas veces, con qué tamaño de entrada y qué datos pueden salir.
Para un proyecto ficticio de una panadería en Guadalajara, podrías permitir que una persona encargada resuma comentarios de pedidos de demostración en MXN, pero rechazar textos que contengan números de tarjeta, contraseñas o información que no sea necesaria. No necesitas inventar un sistema bancario para demostrar una práctica de seguridad; necesitas explicar qué entrada aceptas y por qué.
La parte que suele olvidarse: uso, límites y costo
Una clave expuesta no solo amenaza la confidencialidad. También puede permitir solicitudes no autorizadas y generar consumo inesperado según el proveedor. No presentes una cifra de costo como universal: los precios, modelos, límites y políticas cambian, y deben comprobarse en la fuente oficial correspondiente.
En tu aplicación puedes reducir el riesgo con varias capas: limita la longitud del texto, establece un máximo de solicitudes por usuario o IP, evita reintentos infinitos, registra métricas sin guardar el contenido sensible y define qué ocurre cuando el proveedor devuelve un límite. OWASP recomienda tratar los secretos como un ciclo de vida: creación, almacenamiento, acceso, rotación, revocación y respuesta a compromiso en su Cheat Sheet de gestión de secretos.
Para el escenario didáctico, una tabla de control puede usar cantidades ficticias:
| Control del demo | Valor de práctica | Qué evita |
|---|---|---|
| Longitud máxima del comentario | 2.000 caracteres | Entradas innecesariamente grandes. |
| Límite por usuario de prueba | 20 solicitudes por hora | Uso accidental repetitivo. |
| Presupuesto de demostración | MXN 100 como umbral interno ficticio | Confundir el ejercicio con una tarifa real o dejar consumo sin revisar. |
| Datos de entrada | Pedidos y nombres inventados | Enviar información personal real al proveedor durante la práctica. |
Ese MXN 100 no es un precio estimado de una API ni una promesa de ahorro. Es un umbral de laboratorio para que aprendas a observar uso y a definir una alarma. En un proyecto real, consulta los precios, términos y controles de tu proveedor antes de tomar decisiones.
Qué hacer si la clave ya se filtró
No intentes “protegerla” con un commit nuevo que cambie el archivo. Si una clave apareció en un bundle, una captura de pantalla, un log o un repositorio público, trátala como comprometida. La guía de OpenAI recomienda monitorear el uso y rotar las claves; también indica que una clave expuesta debe revocarse y reemplazarse. El procedimiento exacto depende del proveedor.
La secuencia responsable es:
- Detén el uso de la clave comprometida y revócala en el panel del proveedor.
- Crea una nueva credencial con el menor alcance disponible.
- Busca la clave en el repositorio, historial, bundles, logs y configuraciones de despliegue.
- Elimina el recorrido por el frontend y mueve la llamada al backend.
- Revisa el consumo posterior al incidente y documenta qué se cambió.
Eliminar la línea del archivo actual no necesariamente borra el secreto del historial de Git ni de una imagen ya desplegada. Tampoco supongas que cambiar la contraseña arregla un token que sigue activo. La respuesta debe empezar con revocación, no con una limpieza cosmética del código.
Un caso de revisión para tu equipo remoto
Supón que una persona del equipo en Puebla abre un pull request con el título “Agregar resumen de reseñas”. Otra persona en Ciudad de México revisa el cambio de forma asíncrona. En vez de escribir “parece seguro”, puede dejar preguntas concretas:
## Revisión de seguridad del endpoint
- ¿Qué valor recibe el navegador y qué valor solo lee el servidor?
- ¿La respuesta del backend puede incluir headers del proveedor?
- ¿Qué límite existe por usuario y por tamaño de entrada?
- ¿Qué ocurre si el proveedor responde 429 o tarda demasiado?
- ¿Los logs excluyen la clave y el contenido sensible?
- ¿La prueba usa datos inventados?
Resultado: solicitar cambios antes de aprobar.Esta forma de revisión también ayuda cuando la documentación, los nombres de variables y los mensajes de error están en inglés. Puedes anotar secret exposure como “exposición de secreto”, rate limit como “límite de solicitudes”, backend proxy como “ruta intermedia del servidor” y least privilege como “menor privilegio”. Aprender el término original y su explicación en español te permite buscar mejor sin dejar de comprender lo que estás haciendo.
Si trabajas de forma remota, la claridad importa tanto como el código. La STPS mantiene información oficial sobre condiciones generales de seguridad y salud relacionadas con el teletrabajo en México; esa referencia puede servir para pensar en conectividad, ergonomía y organización del entorno, pero no determina tu contratación ni sustituye orientación laboral o jurídica.
El proyecto de portafolio que demuestra la decisión
Construye un demo pequeño llamado, por ejemplo, “Reseñas de panadería”. Usa entradas ficticias como “pedido-demo-014”, no nombres reales ni datos de clientes. La aplicación puede tener un formulario, un endpoint propio, validación de longitud, límite de solicitudes y un modo de error simulado.
En el README, documenta cuatro pruebas:
| Prueba | Resultado que debes observar | Evidencia para guardar |
|---|---|---|
| Buscar la clave en el bundle | No aparece ninguna credencial privada. | Captura o explicación reproducible sin mostrar secretos. |
| Enviar una entrada demasiado larga | El backend responde 400. | Request y response sin datos personales. |
| Simular un límite del proveedor | La interfaz muestra un error controlado. | Log con identificador de evento, no con la clave. |
| Revisar el repositorio | No hay secretos en el estado ni en la historia destinada a publicar. | Comandos y resultado revisado antes de compartir. |
Este proyecto no prueba que puedas operar cualquier sistema de producción ni garantiza empleo, salario o contratación. También puede servirte leer la guía sobre cómo medir tu autonomía al programar con IA: la seguridad mejora cuando puedes explicar y verificar cada decisión sin depender de una respuesta opaca. Sí muestra una capacidad concreta: sabes reconocer una frontera de confianza, justificar una arquitectura, probar sus límites y explicar una decisión a otra persona.
La pregunta correcta antes de hacer deploy
Antes de publicar una aplicación con IA, no preguntes solamente “¿la clave está en un .env?”. Pregunta: “¿el valor llega al navegador?”, “¿qué puede hacer una persona con él?”, “¿puedo revocarlo?”, “¿qué limita el abuso?”, “¿qué datos envío?” y “¿cómo demostraré que la solución funciona sin revelar información sensible?”.
Si la respuesta a la primera pregunta es sí y la credencial es privada, detén el deploy. Mueve la llamada al servidor, vuelve a generar la clave si fue expuesta, prueba el flujo con datos ficticios y deja la decisión escrita. Una aplicación para una panadería de Guadalajara, un portafolio de principiante o un prototipo de una empresa remota puede ser pequeño; la frontera de seguridad no deja de importar por ser un proyecto de aprendizaje.

Martin Rojas escribe sobre tecnología, programación y desarrollo web. En Skydutz Academy comparte explicaciones prácticas sobre herramientas digitales, conceptos de programación y recursos para aprender de forma progresiva. Su objetivo es hacer que los temas técnicos sean más claros, útiles y accesibles para lectores de distintos niveles.
