Saltar al contenido

Fundamentos de seguridad

¿Tu archivo .env está expuesto? Las doce rutas a revisar

¿Tu archivo .env está expuesto en tu propio servidor web? Doce direcciones te lo dicen, y un acierto significa que todo lo que contiene ya es público.

Vlad Tkachenko10 min de lectura
Cuatro carpetas selladas detrás de un muro y, delante, una carpeta abierta con sus líneas a la vista bajo una franja ámbar.

En resumen

  • Dos problemas distintos se llaman igual: un archivo .env expuesto. Este es el archivo en sí, puesto en tu servidor web y descargable por cualquiera que escriba la dirección.
  • Un host que solo sirve tu frontend ya construido no puede hacer esto. Un servidor de verdad sí, y por eso siete de las ocho aplicaciones que encontramos eran de Replit.
  • De 30.761 aplicaciones en vivo donde nuestra comprobación obtuvo respuesta, 8 estaban entregando un archivo privado. Doce direcciones te dicen si eres la novena.

Alguien te ha dicho que tu archivo .env está expuesto, y por la frase no puedes saber si eso es grave. Cubre dos situaciones completamente distintas. Una de ellas es la herramienta funcionando como debe. La otra significa que un archivo que creías privado se puede descargar desde tu sitio en vivo, por cualquiera, desde el día en que está ahí.

Esta es la parte que guía tras guía mezcla: que un valor de tu .env acabe dentro de tu JavaScript no es el mismo suceso que tu servidor web sirviendo el archivo .env. Lo primero es el build haciendo lo que le pediste cuando llamaste a la variable VITE_ALGO. Lo segundo es un error de archivo, y es lo que conviene mirar antes, porque puedes comprobarlo tú desde fuera en un minuto.

Piensa en tu sitio en vivo como el mostrador de una tienda. Todo lo que hay en el mostrador está para llevarse: la página, las imágenes, el JavaScript, el logotipo. La trastienda guarda lo que hace funcionar el negocio, y un visitante no tiene camino hasta ahí. Un valor compilado dentro de tu JavaScript es una línea impresa en el folleto del mostrador. El archivo .env que responde en una dirección pública es la carpeta de la trastienda, dejada en el mostrador junto a todo lo demás.

¿Mi archivo .env está expuesto?

Abre tu sitio en vivo en un navegador, añade /.env al final de la dirección y pulsa enter.

Pueden volver tres cosas, y solo una de ellas es un problema.

Una página de tu aplicación. La mayoría de los frontends responden a cualquier dirección desconocida con su propia página índice, porque así enruta una aplicación de una sola página. Te sale tu portada, o tu pantalla de no encontrado. En esa ruta no se sirve nada.

Un error. Un 403 o un 404 significa que al servidor se le preguntó y declinó. Esa es la respuesta que quieres.

Texto plano, con líneas dentro. Algo como SUPABASE_URL=https://… y SUPABASE_SERVICE_ROLE_KEY=eyJ…, en un muro monoespaciado sin estilo alguno. El archivo se entrega a quien lo pida, y lleva haciéndolo desde el día en que se sirvió por primera vez.

Dos cosas distintas se llaman archivo .env expuesto

Una va de tu bundle. La otra va de tu servidor.

Qué ha pasadoDónde está el valorQué cuesta arreglarlo
Llamaste a una variable VITE_, NEXT_PUBLIC_ o EXPO_PUBLIC_Compilado en el JavaScript que descarga cada visitanteMover el trabajo a un servidor y rotar la clave. El archivo nunca se sirvió.
Tu servidor web publica tu carpeta de proyectoEn el archivo, en https://tusitio/.envSacar el archivo de lo que publica el servidor y rotar todo lo que contenía.

La primera fila es la común y tiene su propio artículo: el prefijo es una instrucción de publicar, y el build la obedeció. Todo lo de abajo es la segunda fila.

Izquierda: el build copió un valor a tu JavaScript, porque el prefijo se lo pidió. Derecha: el servidor entrega el archivo, y los prefijos ahí no cambian nada.

Por qué una aplicación de Lovable no puede y una de Replit sí

Porque los dos hosts publican cosas distintas.

Un builder que despliega un frontend estático le entrega a su host una carpeta de archivos construidos: algo de HTML, algo de JavaScript, algunas imágenes. Tu .env se leyó durante el build y no está en esa carpeta, así que en /.env no hay nada que nadie pueda pedir. El host no podría servir el archivo ni queriendo.

Una aplicación de Replit suele ejecutar su propio servidor, en su propia carpeta de proyecto, con tus archivos al lado de tu código. A un servidor al que se le dice que sirva su carpeta sirve todos los archivos que hay en ella, y no tiene manera de saber que uno de ellos guarda tus claves. Lo mismo vale para cualquier cosa que hayas desplegado en una máquina tuya.

Los números siguen a la arquitectura. De 30.761 aplicaciones en vivo donde esta comprobación obtuvo respuesta, 8 entregaban al menos un archivo privado. Siete de las ocho eran de Replit, y las de Replit eran 3.025 de las 30.761. La octava estaba en un dominio propio, que dice lo mismo de otra manera: alguien tenía su propio servidor.

Es el hallazgo más raro que imprimimos, y el único de los nueve sin explicación inocente. Si quieres la lectura amplia de lo que entregan de verdad las aplicaciones de Replit, escaneamos 3.042 de ellas.

Las doce rutas a revisar

Doce direcciones, en el orden que merece la pena. Pon cada una detrás de tu dominio.

DirecciónQué guardaSi responde con texto
/.envTodas las claves con las que se construyó tu appTrátalo todo como público y rota
/.env.localLo mismo, de una ejecución localLo mismo
/.env.productionLo mismo, de tu despliegue en vivoLo mismo
/.git/configLa dirección remota de tu repositorioTodo el directorio .git suele ser legible
/.git/HEADEn qué rama estásLo mismo, y es la más silenciosa de las doce
/.aws/credentialsClaves de Amazon de larga duraciónRota en Amazon y luego revisa la factura
/database.sqlEsquema y filasTodas las filas de todas las tablas son públicas
/dump.sqlLo mismoLo mismo
/backup.sqlLo mismoLo mismo
/config.jsonLo que hayas puesto dentroLéelo y mira; hay tokens ahí más a menudo de lo que se cree
/docker-compose.ymlDefiniciones de servicios, a menudo con contraseñasRota todo lo que esté escrito dentro
/.npmrcUn token de registroRevoca el token

Nuestro propio escaneo lee las doce desde fuera y te nombra las que respondieron. Tarda unos 20 segundos y no necesita cuenta: escanea tu aplicación.

Qué significa de verdad un acierto

Todo lo que hay en ese archivo es público ahora mismo, y lo es desde el día en que se sirvió por primera vez.

No vas a averiguar quién lo leyó. Un builder no te da un registro de archivos descargados que puedas ir a mirar, y la petición se ve igual que cualquier otra petición de cualquier otro archivo. De lo que sí puedes estar seguro es de que alguien lo intentó: hay crawlers automáticos recorriendo exactamente estas doce rutas por todo internet, sin parar, y no necesitan saber quién eres para dar con la tuya.

Cinco de las ocho aplicaciones que encontramos entregaban una de las cuatro caras de inmediato: .env, un directorio .git, .aws/credentials o un dump .sql. Las otras tres entregaban config.json, docker-compose.yml o .npmrc. Esas tres son las que todo el mundo da por inofensivas, que es justo por lo que acaban con tokens y contraseñas de base de datos escritos dentro.

Lo que esto no es, es un veredicto sobre tu aplicación. Una comprobación externa lee lo que tu sitio le entrega a un desconocido, y doce rutas que responden con nada son doce rutas que responden con nada. Qué más se escapa de una aplicación vibe-codeada es la lista larga.

La que arruina una semana: un directorio .git

Un directorio .git no es un archivo. Es todo tu historial.

Ahí está cada commit, incluido aquel en el que pegaste una clave y el posterior en el que la quitaste. Esa es la parte que la gente entiende mal de una fuga de .git: borrar un secreto del código que tienes hoy no hace nada con la versión donde seguía estando, y esa versión está en el mismo directorio que tu servidor publica.

La comprobación lee /.git/config y /.git/HEAD porque son pequeños, su contenido es inconfundible y cualquiera de los dos respondiendo significa que el directorio en sí se está sirviendo. A partir de ahí quien lee no necesita ninguna herramienta especial. El formato está documentado y software corriente lo clona.

Si una clave estuvo alguna vez en un commit, rotarla es lo único que ayuda. Cuál va primero depende de si la copia ya está fuera, y ese orden merece la pena acertarlo.

El dump que alguien dejó en la carpeta

Tres de las doce rutas son volcados de base de datos: /database.sql, /dump.sql y /backup.sql.

Un dump son todas las filas de todas las tablas en un archivo, con el esquema encima. Direcciones de correo, contraseñas con hash, pedidos, mensajes, lo que guarde tu aplicación. Responde en una dirección pública por el motivo más aburrido de todo este artículo: alguien hizo lo responsable, sacó una copia de su base de datos y la guardó en la carpeta de proyecto donde estaba.

Así que el archivo hecho para proteger los datos se convirtió en la vía más rápida de leerlos todos. Dónde vive una copia es tanta decisión como si la haces. Tres formas de respaldar una base de datos Supabase repasa las opciones, y Reeve Care guarda una copia de tu base de datos de Supabase fuera de tu propio servidor, verificada antes de contar como copia.

Cómo arreglarlo, y por qué importa el orden

Saca el archivo de lo que publica tu servidor. Después rota cada secreto que tenía dentro. En ese orden.

Rotar primero parece la mitad urgente, y es la mitad que desperdicia el trabajo. Tus claves nuevas van al mismo archivo, el archivo sigue respondiendo en la misma dirección, y has rotado directamente de vuelta a la fuga. Nada está más seguro que hace diez minutos.

Rota primero y la clave nueva aterriza en el archivo que se sigue entregando. Mueve primero y ya no queda nada que entregar.

Dónde ponerlo en su lugar, según lo que tengas montado:

  • Un host con su propio almacén de secretos. Replit Secrets, las variables de entorno de una plataforma, tu panel de hosting. Tu servidor lee el valor en tiempo de ejecución y nunca queda en un archivo dentro de la carpeta publicada.
  • Fuera de la carpeta servida. Si controlas la configuración del servidor, apúntalo a una carpeta de salida del build en lugar de a la raíz del proyecto. Entonces un archivo en la raíz no tiene dirección ninguna.
  • Tampoco en el repositorio, que es una costumbre aparte y buena. Con el problema de hoy no hace nada.

Ese último punto conviene decirlo claro, porque .gitignore es la respuesta a la que todo el mundo va. Mantiene el archivo fuera de tu repositorio. El archivo de tu servidor llegó ahí porque el servidor está sentado en tu carpeta de proyecto, y git no tiene opinión sobre eso.

Qué hacer ahora mismo

Qué hacer

  • Escribe las doce direcciones detrás de tu propio dominio, o lanza el escaneo gratuito y que escriba él. Lee lo que vuelve, no el código de estado.
  • Si una responde con tus propios ajustes, saca el archivo de la carpeta que publica tu servidor y vuelve a desplegar. Ese es el paso que lo corta.
  • Después rota cada clave, contraseña y token que tenía el archivo, en cada proveedor. Rotar antes de mover el archivo devuelve los valores nuevos a donde estaban los viejos.
  • Revisa facturación y registros del proveedor al terminar. Rotar corta lo que viene después y no hace nada con lo que ya pasó.
  • Si un directorio .git era legible, rota todo lo que estuvo alguna vez en un commit, no solo lo que hay hoy en el código.

Si prefieres recorrer tu aplicación como una lista, la checklist de seguridad de 10 minutos cubre esto junto a las otras cosas que conviene cerrar en una aplicación recién lanzada.

Preguntas frecuentes

¿Cómo sé si mi archivo .env es público?

Abre tu sitio en vivo en un navegador, añade /.env al final de la dirección y pulsa enter. Si vuelve una página de tu aplicación, no se está sirviendo nada en esa ruta. Si vuelve texto plano, con líneas como SUPABASE_URL=https://abcdefghij.supabase.co, el archivo se entrega a quien lo pida. Haz lo mismo con /.env.local y /.env.production, porque un servidor que publica uno suele publicar los tres.

¿Un archivo .env en mi bundle es lo mismo?

No, y la solución es otra. Un valor que acaba dentro de tu JavaScript llegó ahí porque al build se le pidió, con una variable llamada VITE_ o NEXT_PUBLIC_ o EXPO_PUBLIC_. El archivo nunca salió de tu máquina; el valor sí. Ese es un problema aparte y tiene su propio artículo. Este va del archivo en sí, servido por tu servidor web, lo que deja público cada valor que contiene, lleve prefijo o no.

¿Por qué alguien puede descargar mi carpeta .git?

Porque tu servidor apunta a tu carpeta de proyecto y .git es un directorio dentro de ella como cualquier otro. Un servidor no sabe que uno de ellos es tu historial de versiones. Si /.git/config o /.git/HEAD responde con texto, todo el directorio suele ser legible, y eso incluye cada commit que hiciste, no solo el código que tienes hoy.

He encontrado un backup.sql en mi propio sitio, ¿y ahora?

Sácalo de la carpeta que publica tu servidor antes de hacer nada más, porque el archivo se está entregando mientras lees esto. Después trata como público cada contraseña, clave y dato personal que contenga: un dump lleva el esquema y todas las filas de todas las tablas. Y averigua cómo llegó ahí, que suele ser alguien que hizo una copia en la carpeta del proyecto y nunca la movió.

¿Roto las claves o borro el archivo primero?

Mueve el archivo primero y rota después. Rotar mientras el archivo se sigue sirviendo escribe los valores nuevos en algo que cualquiera puede descargar, así que gastas el trabajo y acabas donde estabas. Cuando ya no responda nada en esa ruta, rota cada secreto que tenía el archivo, en cada proveedor, y revisa facturación y registros al terminar.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

Todos los artículos

¿No sabes cómo está tu propia app?

Haz un escaneo gratuito y obtén una nota clara de la A a la F en unos 20 segundos. Sin cuenta y sin tarjeta.

Analizar mi app gratis

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.