Guías de seguridad

¿Dejaste un archivo de secretos en un directorio público? Audita tu webroot

Un token, un .env o una copia de seguridad en el directorio público de tu web se descarga por URL. Cómo auditar tu servidor y sacar los secretos del webroot.

Publicado 2026-06-11 Actualizado 2026-10-06 17 min de lectura
En esta página

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.

Los archivos del directorio público los puede descargar cualquiera vía URL. Mantén los secretos fuera de él (no servidos, permisos 600).

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.

una URL
un archivo del webroot es descargable por cualquiera
años
permanece inadvertido
cada sitio
lo olvidado en una plantilla se extiende a todos los sitios
revoca ya
asume que se filtró y rota

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.

TipoNombres típicosQué está en riesgoDónde debe estar
Archivos de configuración.env, config.php.bak, settings.jsonContraseñas de base de datos y claves de API escritas en texto planoPor encima de la raíz de la app (no servido)
Tokens y credencialestoken.json, credentials.json, *.pem, *.keyAcceso directo a un servicio externo o a otro servidorFuera del webroot, permisos 600
Copias manuales o del editorlogin.php.old, index.php~, .index.php.swp, *.origCon la extensión cambiada, el archivo puede no ejecutarse como código, así que su código fuente se muestra como texto planoNo las crees en producción; edita en local
Volcados de bases de datosbackup.sql, dump.sql.gzDatos personales de los usuarios y hashes de contraseñas, todo en un archivoUn lugar dedicado a copias, idealmente fuera del servidor
Archivos comprimidos del sitiosite.zip, www.tar.gzContienen el .env y todo el código fuenteBórralos justo después de descomprimir
Datos de control de versiones.git/Pueden recuperarse secretos de commits antiguosFuera del directorio público
Valores insertados en el builddentro de assets/index-xxxx.jsLas variables que empiezan por VITE_ o NEXT_PUBLIC_ acaban en el JavaScript del navegadorGuarda 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.

1

Busca bajo los directorios públicos archivos con aspecto de secreto

A lo largo de tu propio home, saca a la luz candidatos a secreto que se colaron en public/ (JSON de token/credenciales, .env, claves, atajos).
2

Asume que el mismo archivo existe en otros sitios — revisa cada servidor y sitio

Los sitios construidos a partir del mismo boilerplate tienen lo mismo en el mismo sitio; ejecuta la misma comprobación en todos los servidores y sitios. Una coincidencia suele significar que hay más.
3

Decide por archivo si de verdad necesita ser público

Clasifica cada resultado en "seguro publicar" vs "secreto". Los secretos pasan por los pasos de quitar/revocar/mover de abajo.
# 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/null

Có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.

1

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.

2

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.

3

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_).

4

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.

5

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.

1

Quita del webroot para que la URL no pueda descargarlo

Mueve el archivo fuera del directorio público o bórralo, y confirma que la URL ahora devuelve 404.
2

Revoca/reemite cualquier clave o token que pueda haberse filtrado

No lo dejes en "probablemente está bien". Como quedó expuesto, asume que se filtró y renueva o revoca ese token/clave.
3

Mantén los secretos fuera del webroot con permisos 600

De ahora en adelante, coloca los secretos fuera del directorio público (p. ej. por encima de la raíz de la app) con permisos 600 (solo lectura del propietario). Si tu configuración puede denegar servir extensiones sensibles, añádelo también.

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ónCómo denegarListado de directoriosTen en cuenta
Apache (VPS)<FilesMatch> + Require all denied en un bloque <Directory> o en .htaccessDesactívalo con Options -IndexesEn Apache 2.4, AllowOverride es None por defecto, así que .htaccess se ignora salvo que lo permitas
Nginx (VPS)deny all; dentro de un locationautoindex es off por defectoNo lee .htaccess. Los bloques location con regex se comprueban en el orden en que están escritos y gana la primera coincidencia
Hosting compartidoNormalmente .htaccess (compatible con Apache)Desde el panel de control o .htaccessConfirma que funciona con un archivo de prueba que no contenga secretos
Hosting estático / CDNLo que haya en la carpeta de salida se publica tal cualDepende del servicioConfí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 .git dejadas 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 .htaccess en 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 .git y 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_ o NEXT_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

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" (AllowOverride es None por defecto) y "mod_autoindex" (Options +Indexes genera listados): httpd.apache.org, httpd.apache.org
  • nginx, "ngx_http_core_module: location" (orden de coincidencia y ^~) y "ngx_http_autoindex_module" (autoindex es off por 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

FAQ

Q¿Por qué es peligroso poner archivos en un directorio público?
A

Cualquier cosa en el directorio público de la web (webroot — public/, public_html/, etc.) puede descargarla quien acierte con su URL. Un JSON de token/credenciales, un .env, una copia de seguridad o un archivo de clave olvidados se recuperan antes de que nadie se dé cuenta, provocando un daño inmediato. El directorio público debería contener solo cosas que es seguro que sean públicas.

QSi encuentro uno en un sitio, ¿debería revisar los demás?
A

Sí. Si produces sitios en masa a partir de una plantilla o boilerplate compartido, lo olvidado suele estar copiado en todos ellos. Encuentra uno y audita cada sitio y servidor del mismo origen.

Q¿Qué hago cuando encuentro uno?
A

1) Quita el archivo del webroot para que devuelva 404 por URL. 2) Renueva/revoca cualquier token o clave que pueda haberse filtrado (asume que se filtró). 3) En adelante, mantén los secretos fuera del webroot con permisos 600 (solo lectura del propietario). Haz las tres cosas como un conjunto.

Q¿Qué archivos no deberían estar nunca en un directorio web público?
A

Los sospechosos habituales son archivos de configuración como .env, archivos JSON de tokens o credenciales, volcados de bases de datos (.sql), archivos ZIP o tar.gz con todo el sitio, el directorio .git y las copias de seguridad hechas a mano o por un editor (.bak, .old, .orig, .swp, o un nombre que termina en ~). La guía de pruebas de OWASP explica que si login.php se guarda como login.php.old, el servidor puede no ejecutarlo como código y devolver en su lugar el código fuente como texto plano.

QHe borrado el archivo pero sigue apareciendo en los resultados de búsqueda. ¿Qué hago?
A

Primero asegúrate de que la URL devuelve 404 o 410, y después usa la herramienta de retirada de URLs de Google Search Console para ocultarla temporalmente de los resultados. Según la página de ayuda de Google, una retirada temporal dura unos seis meses y está pensada para combinarse con una solución permanente, como borrar el archivo. La IPA de Japón también aconseja pedir a los buscadores que eliminen las copias en caché. Revoca cualquier clave o token que contuviera el archivo, haya salido o no de los resultados de búsqueda.

Q¿Puedo poner secretos en variables de entorno del frontend (VITE_ o NEXT_PUBLIC_)?
A

No. Vite inserta las variables que empiezan por VITE_, y Next.js las que empiezan por NEXT_PUBLIC_, en el JavaScript que se envía al navegador en el momento del build. Es lo mismo que colocar un archivo en el directorio público: cualquiera puede leerlo. Guarda las claves de API y demás secretos en variables que solo use el código del servidor.