Evitar que una clave de servicio llegue al navegador
Next.js incrusta en el paquete de cliente, en tiempo de compilación, cualquier variable
de entorno con el prefijo NEXT_PUBLIC_. Vite hace lo mismo con
VITE_, y Create React App con REACT_APP_. Ese es el comportamiento
documentado y buscado: esos prefijos existen para marcar un valor como público.
El fallo es poner un secreto detrás de uno de ellos. La credencial es real y tiene el formato correcto; simplemente está en la compilación equivocada. Cualquiera abre las herramientas del navegador y la lee.
Qué claves pueden ir detrás del prefijo y cuáles no
| Seguras — pensadas para ser públicas | No seguras — dan acceso al servidor |
|---|---|
NEXT_PUBLIC_SUPABASE_ANON_KEY | NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | NEXT_PUBLIC_STRIPE_SECRET_KEY |
NEXT_PUBLIC_FIREBASE_API_KEYpública por diseño; se protege con reglas |
VITE_OPENAI_API_KEYfactura contra tu cuenta, y no hay regla que la proteja |
Cómo comprobar tu proyecto en un minuto
# cualquier variable con prefijo de navegador cuyo nombre sugiera un secreto
grep -rEn "(NEXT_PUBLIC_|VITE_|REACT_APP_)[A-Z_]*(SECRET|SERVICE_ROLE|PRIVATE|API_KEY|TOKEN)" \
.env* src app lib 2>/dev/null
Y luego lee cada coincidencia, porque dos de ellas no van a ser fallos:
Los dos casos que nos costaron una disculpa pública
Estábamos preparando avisos de seguridad para repositorios públicos, y comprobamos cada uno a mano antes de enviarlo. Los dos estaban mal, y los dos están ahora fijados por una prueba de regresión escrita con la línea exacta:
1. Una variable forzada a vacío es el arreglo, no el fallo
...(command === "build"
? { "import.meta.env.VITE_OPENAI_API_KEY": '""' }
: {}),
Esa línea elimina la clave de todas las compilaciones de producción. Un
comentario de seis líneas encima explica que Vite incrusta las VITE_* en el
paquete. Le habríamos dicho a un equipo que entiende esto mejor que nuestro escáner que
tenía el fallo que ya había arreglado.
2. Leer una variable en Node no es publicarla
module.exports = {
openAIApiKey: process.env.VITE_OPENAI_API_KEY,
}
La configuración de una herramienta de traducción. Corre en Node en tiempo de
compilación. El prefijo VITE_ despista: el código de cliente nunca referencia
la variable, así que nada llega al navegador. Cualquier escáner que busque el prefijo va a
marcar esto, y se va a equivocar.
Cómo se arregla
- Rota la clave primero. Da por hecho que está comprometida: el paquete es público y puede estar cacheado o archivado. Limpiar el historial lleva horas; la clave está viva durante todas ellas.
- Muévela a una variable sin el prefijo, leída solo desde código de servidor —un route handler, una server action, una ruta de API.
- Si el navegador necesita de verdad llegar al proveedor, haz de intermediario con tu propio backend, o usa la clave publicable, que existe exactamente para esto.
Lo que medimos, y publicamos
| Medición | Resultado |
|---|---|
| Primera ejecución automática sobre 249 pull requests públicos | 150 hallazgos, 79 CRÍTICOS — casi ninguno real |
| La misma población tras cuatro rondas de correcciones | 39 hallazgos, 0–1 CRÍTICO |
| Dependencias resueltas en vivo contra PyPI y npm | la cifra en vivo |
| Paquetes alucinados por IA confirmados | 0 |
Esa última fila juega en nuestra contra: construimos esto alrededor de los nombres de
paquete inventados, lo medimos, y el ataque resulta ser más raro de lo que sugiere el sector.
Un paquete que no existe hace que pip install falle y que el CI se ponga rojo,
gratis. Lo publicamos porque un proveedor al que puedes comprobar vale más que un proveedor
al que tienes que creer.
Última revisión: 21 de septiembre de 2026. Todas las cifras de esta página son medición propia y reproducibles contra nuestro endpoint público.