Para: cualquiera que ejecute varios sitios en hosting compartido/de alquiler o un VPS, desplegando en directorios públicos (public/, public_html/, htdocs/, etc.). Sin pasos de ataque — solo auditar tu propio servidor y cerrar los agujeros. Para proteger el propio .env, ver proteger el .env en hosting compartido.
La visión de este sitio: dónde lo pones ES el control de acceso
Los secretos se filtran más por accidentes de ubicación que por ataques ingeniosos. El webroot es "un sitio del que cualquiera puede venir a descargar por URL". Pon un secreto ahí y hasta la contraseña de servidor más fuerte deja de tener sentido. La regla básica de este sitio es mantener los secretos fuera tanto del webroot como de git (→ guardar secretos con seguridad). La idea es simple: pon en el directorio público solo cosas que no te importe que vea cualquiera.
public/ = descargable por cualquiera vía URL
OK colocar
imágenes, CSS, JS — recursos públicos
Nunca colocar
.env, token.json, .sql, .git, .bak
Por encima de la raíz de la app = no servido
Pon aquí
secretos, tokens, credenciales, copias de seguridad. Permisos 600 (solo lectura del propietario). Inalcanzable por URL.
Por qué ocurren los "restos en el directorio público"
El patrón común es un archivo colocado por comodidad, o uno que vino dentro de un boilerplate, que se queda público en silencio.
Por ejemplo, un JSON que contiene un token de acceso para una integración de terceros permanece bajo public/ durante mucho tiempo — y si es de una plantilla compartida, el mismo archivo se copia al mismo sitio en muchos sitios web, así que todos comparten el mismo problema. Arreglar un sitio no sirve de nada si los demás siguen teniendo el archivo. Igual de peligrosos: .env olvidados, copias de seguridad de bases de datos (.sql), un directorio .git, copias de seguridad del editor (*.bak) y atajos (relacionado: path traversal · un .env entero expuesto).
Archivos que se quedan olvidados, y por qué cada uno es peligroso
"Archivo de secretos" suele hacer pensar en .env, pero lo que de verdad se queda olvidado es mucho más amplio. La guía de pruebas de OWASP (una organización sin ánimo de lucro centrada en la seguridad web) también incluye las versiones antiguas de archivos, las copias de seguridad y los archivos a los que nada enlaza como cosas que revisar.
| Tipo | Nombres típicos | Qué está en riesgo | Dónde debe estar |
|---|---|---|---|
| Archivos de configuración | .env, config.php.bak, settings.json | Contraseñas de base de datos y claves de API escritas en texto plano | Por encima de la raíz de la app (no servido) |
| Tokens y credenciales | token.json, credentials.json, *.pem, *.key | Acceso directo a un servicio externo o a otro servidor | Fuera del webroot, permisos 600 |
| Copias manuales o del editor | login.php.old, index.php~, .index.php.swp, *.orig | Con la extensión cambiada, el archivo puede no ejecutarse como código, así que su código fuente se muestra como texto plano | No las crees en producción; edita en local |
| Volcados de bases de datos | backup.sql, dump.sql.gz | Datos personales de los usuarios y hashes de contraseñas, todo en un archivo | Un lugar dedicado a copias, idealmente fuera del servidor |
| Archivos comprimidos del sitio | site.zip, www.tar.gz | Contienen el .env y todo el código fuente | Bórralos justo después de descomprimir |
| Datos de control de versiones | .git/ | Pueden recuperarse secretos de commits antiguos | Fuera del directorio público |
| Valores insertados en el build | dentro de assets/index-xxxx.js | Las variables que empiezan por VITE_ o NEXT_PUBLIC_ acaban en el JavaScript del navegador | Guarda los secretos solo en variables del lado del servidor |
La última fila es fácil de pasar por alto. Aunque nunca hayas colocado un archivo, un build puede meter un secreto en el directorio público. La documentación oficial de Vite dice claramente que las variables VITE_ no deben contener información sensible como claves de API.
Audita tu propio servidor
Primero, saca a la luz de forma mecánica "cualquier archivo con aspecto de secreto en mi propio directorio público". Esto es inspeccionar tus propios activos, no fisgonear el sitio de otra persona.
Busca bajo los directorios públicos archivos con aspecto de secreto
public/ (JSON de token/credenciales, .env, claves, atajos).Asume que el mismo archivo existe en otros sitios — revisa cada servidor y sitio
Decide por archivo si de verdad necesita ser público
# audit under your own public directories (inspecting your own assets)
find ~ -path '*/public/*' \( -iname '*token*.json' -o -iname '*credential*.json' \
-o -iname '*.bak' -o -iname '*.sql' -o -iname '.env*' \) 2>/dev/null
# an exposed .git directory is dangerous too
find ~ -path '*/public/*/.git' -maxdepth 8 2>/dev/nullCómo revisar tu propio entorno
El find de arriba es un punto de partida. Repasar las cinco comprobaciones de abajo detecta restos que una búsqueda solo por nombre de archivo no encuentra. Ejecútalas solo contra tu propio servidor y tu propio dominio. Las comprobaciones específicas del .env (mirar los primeros bytes de la respuesta, confirmar que .htaccess funciona de verdad) se explican en proteger el .env en hosting compartido.
Busca también copias de seguridad y archivos comprimidos
Los nombres del directorio público varían según el hosting, así que incluye public_html, htdocs y www. Por ejemplo, ejecuta find ~/public_html \( -name '*~' -o -name '*.swp' -o -name '*.old' -o -name '*.orig' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null y confirma, archivo por archivo, que cada resultado de verdad debe ser público.
Asegúrate de que el listado de directorios está desactivado
Abre en un navegador un directorio que no tenga index.html (por ejemplo /uploads/). Si ves una lista de nombres de archivo, la función de listado de directorios del servidor web está activada. Un listado permite descubrir restos sin conocer sus nombres. Lo que quieres es un 403 o un 404.
Busca secretos en el JavaScript compilado
Si compilas un frontend, busca en la carpeta de salida que despliegas (dist/, .next/static/, etc.). Usa algo como grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/, más el prefijo que usa tu servicio para sus claves. Si encuentras uno, comprueba si la variable tiene un nombre de cara al navegador (VITE_, NEXT_PUBLIC_).
Comprueba que los secretos fuera del webroot no los pueden leer otros usuarios
Sacar un secreto del webroot no sirve si unos permisos laxos dejan que otra cuenta del mismo servidor lo lea. find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null lista los secretos que puede leer cualquiera que no sea el propietario; corrígelos con chmod 600. Si no sale nada, estás bien.
Comprueba si los buscadores han indexado algo
Busca en Google site:tudominio y busca archivos que no reconozcas (.sql, .zip, .txt, etc.). Si usas Search Console, el informe de Páginas lista las URLs que ha indexado. Qué hacer si algo sigue en los resultados de búsqueda se explica en los errores habituales de más abajo.
Qué hacer cuando encuentras uno
Haz tres cosas como un conjunto — quitar, revocar, mover. Sáltate una y la corrección queda incompleta.
Quita del webroot para que la URL no pueda descargarlo
Revoca/reemite cualquier clave o token que pueda haberse filtrado
Mantén los secretos fuera del webroot con permisos 600
Por servidor: ajustes que se niegan a servir estos archivos (como red de seguridad)
La solución real es no colocar ahí los archivos. Además de eso, añade una regla de denegación en el servidor web para que lo que se coloque por error siga sin servirse. Lo que puedes usar depende de tu configuración.
| Configuración | Cómo denegar | Listado de directorios | Ten en cuenta |
|---|---|---|---|
| Apache (VPS) | <FilesMatch> + Require all denied en un bloque <Directory> o en .htaccess | Desactívalo con Options -Indexes | En Apache 2.4, AllowOverride es None por defecto, así que .htaccess se ignora salvo que lo permitas |
| Nginx (VPS) | deny all; dentro de un location | autoindex es off por defecto | No lee .htaccess. Los bloques location con regex se comprueban en el orden en que están escritos y gana la primera coincidencia |
| Hosting compartido | Normalmente .htaccess (compatible con Apache) | Desde el panel de control o .htaccess | Confirma que funciona con un archivo de prueba que no contenga secretos |
| Hosting estático / CDN | Lo que haya en la carpeta de salida se publica tal cual | Depende del servicio | Confía en mantener los secretos fuera de la carpeta de salida, no en reglas de denegación |
Ejemplo para Apache (donde .htaccess está habilitado):
Options -Indexes
<FilesMatch "(^\.|~$|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key)$)">
Require all denied
</FilesMatch>Ejemplo para Nginx (dentro de un bloque server):
# Allow the path used for automatic certificate renewal (Let's Encrypt) first
location ^~ /.well-known/acme-challenge/ { }
# Deny files and directories starting with a dot (.env, .git, ...)
location ~ /\. { deny all; }
# Deny backups and archives
location ~* (~|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key))$ { deny all; }En Nginx, cuando una petición coincide con un location marcado con ^~, no se comprueban los locations con regex. Así, /.well-known/acme-challenge/, usado para la renovación de certificados, se sigue sirviendo pese a la regla que deniega las rutas que empiezan por un punto. Tras cambiar la configuración, comprueba la sintaxis con nginx -t, recarga y confirma que un archivo de prueba devuelve 403. Si tu sitio ofrece archivos ZIP o similares para descargar, quita esa extensión de la lista de denegación.
La ubicación en la que cae la gente (peligrosa)
- un token JSON para una integración está en
public/ - un archivo de secretos que vino en el boilerplate se queda público
- copias de seguridad (
.sql/.bak) o.gitdejadas bajo el directorio público - arreglar un sitio y darlo por "resuelto"
La ubicación correcta
- el directorio público contiene solo cosas compartibles públicamente
- los secretos viven fuera del webroot con permisos 600
- las extensiones sensibles están denegadas de ser servidas
- encuentra uno y audita cada servidor y sitio
Convierte 'solo cosas seguras para publicar' en una regla permanente
Las reglas vencen a los arreglos puntuales. Decide que el directorio público solo contiene archivos seguros para publicar, y como regla nunca pongas ahí secretos, copias de seguridad, datos de control de versiones (.git) ni archivos de configuración. Cuando actualices un boilerplate, quita el secreto de la propia plantilla — de lo contrario, cada sitio que produzcas en masa después seguirá copiando el mismo archivo de secretos.
Errores habituales y cómo corregirlos
- Confiar en una regla de denegación y dejar el archivo en su sitio: un cambio de servidor o una reescritura de la configuración puede perder la regla, y desde ese día el archivo es público. Nginx no lee
.htaccessen absoluto. Trata la regla como red de seguridad y saca el archivo del directorio público. - Editar archivos directamente en el servidor de producción: los editores dejan copias que terminan en
~o.swp. La guía de pruebas de OWASP también desaconseja editar archivos directamente en el servidor web. Edita en local y publica los cambios mediante tu despliegue. - Copiar toda la carpeta del proyecto al directorio público: el
.env, el.gity los datos de prueba salen con ella. Sirve solo la carpeta de salida del build, y especifica exactamente qué se copia. - Denegar todas las rutas con punto y romper la renovación de certificados: Let's Encrypt coloca su archivo de comprobación bajo
/.well-known/acme-challenge/. Como en el ejemplo de Nginx de arriba, permite primero esa ruta. - Poner secretos en variables
VITE_oNEXT_PUBLIC_: se insertan en el JavaScript compilado para que cualquiera lo lea. Úsalos solo en código del servidor, y revoca cualquier clave que ya hayas publicado así. - Borrar el archivo y quedarse ahí: si un buscador lo indexó, puede seguir apareciendo en los resultados. Confirma que la URL devuelve 404 o 410, y después ocúltala temporalmente (unos seis meses) con la herramienta de retirada de URLs de Google Search Console mientras el nuevo rastreo la elimina.
Un caso real: datos personales ocultos en un informe publicado
El Departamento de Financiación de la Atención Sanitaria (DHCF), que gestiona Medicaid (cobertura sanitaria pública para personas con ingresos bajos) en Washington, D.C., dijo que dos informes estadísticos publicados en su web desde 2023 hasta julio de 2026 contenían los datos personales de base como «campos ocultos». Los informes estaban hechos para mostrar solo cifras agregadas, pero los archivos seguían conteniendo los números de Medicaid de los beneficiarios, sus fechas de nacimiento y más. Su notificación al Departamento de Salud y Servicios Humanos de EE. UU. (HHS) cifró en 399.086 el número de afectados. El DHCF supo del problema el 21 de julio de 2026 y retiró los informes.
No es un caso de archivo de secretos olvidado, pero la raíz es la misma:
- Un archivo en un directorio público puede descargarse completo, no solo la parte que se muestra en pantalla
- La información que quien publica cree «no visible» es la más fácil de pasar por alto en una revisión
- Puede seguir publicado durante años antes de que nadie se dé cuenta
Un paso que compruebe «qué hay de verdad dentro de este archivo» antes de publicarlo evita tanto los archivos olvidados como los datos ocultos. El relato completo está en la exposición de datos de Medicaid en Washington, D.C. (en inglés).
Qué hace este sitio
La regla básica de este sitio es mantener los secretos — claves, tokens, cadenas de conexión — fuera tanto del directorio público como del repositorio de código. Los artefactos de despliegue contienen solo recursos públicos compilados; los secretos se guardan como variables de entorno del entorno de ejecución, en un sitio que no se sirve. La razón: la clase de incidente de este artículo es el caso típico en que un solo archivo olvidado lo filtra todo a cualquiera que solicite su URL. Diseñamos la propia ubicación como control de acceso, y ejecutamos esta auditoría con la misma cadencia que el inventario de respuesta a vulnerabilidades.
Sigue leyendo
- Stacks: proteger el .env en hosting compartido · el manual de respuesta a vulnerabilidades (inventario)
- Glosario: archivos .env y secretos · path traversal
- Incidente: un .env entero expuesto
- Almacenamiento: guardar secretos con seguridad
- En la nube: buckets públicos (en inglés; el mismo problema, fuera del servidor)
- Cuando el proveedor sufre una brecha: qué hacer cuando tu hosting web se ve comprometido (en inglés; asume que lo que hay ahí se ha leído)
- Caso real: números de Medicaid olvidados en informes publicados (Washington, D.C.) (en inglés; un archivo publicado expuesto por completo)
Fuentes
- IPA, "20 Checkpoints for Secure Website Operation and Management" (en japonés; archivos que no deberían ser públicos, cachés de buscadores): ipa.go.jp
- OWASP, "Web Security Testing Guide: Review Old Backup and Unreferenced Files for Sensitive Information" (copias .old, ~, .swp; no editar directamente en el servidor): wstg.owasp.org
- Apache HTTP Server 2.4, "Upgrading to 2.4 from 2.2" (
AllowOverrideesNonepor defecto) y "mod_autoindex" (Options +Indexesgenera listados): httpd.apache.org, httpd.apache.org - nginx, "ngx_http_core_module: location" (orden de coincidencia y
^~) y "ngx_http_autoindex_module" (autoindexesoffpor defecto): nginx.org, nginx.org - Let's Encrypt, "Challenge Types" (HTTP-01 usa
/.well-known/acme-challenge/): letsencrypt.org - Vite, "Env Variables and Modes" (las variables
VITE_se exponen al código del cliente): vite.dev - Next.js, "Environment Variables" (las variables
NEXT_PUBLIC_se insertan en el JavaScript del navegador): nextjs.org - Ayuda de Google Search Console, "Removals tool" (la retirada temporal dura unos seis meses): support.google.com
- District of Columbia DHCF, "Notice of Data Incident": dhcf.dc.gov; HIPAA Journal (cifra notificada al HHS): hipaajournal.com