Guías de seguridad

Un caso en que el .env de Laravel era legible públicamente: un error de ubicación en hosting compartido y cómo corregirlo

En hosting compartido, .env, .git y volcados de BD de Laravel quedan legibles por HTTP si todo el proyecto está bajo la raíz web. La corrección en tres pasos.

Publicado 2026-06-07 7 min de lectura
En esta página

Una mala configuración que sí ocurre en hosting compartido, con dominios y valores eliminados y convertida en lección. Esto no trata de encontrar los sitios de otras personas — trata de asegurar tu propio despliegue (o uno heredado).

Expediente del caso — INCIDENT FILE
Clase
Exposición de .env / .git / volcados de BD (mala configuración)
Gravedad
Crítica (todos los secretos legibles por HTTP)
Causa
Cuerpo de Laravel colocado directamente bajo public_html (solo public/ debería exponerse)
Propagación
Cada nueva app copiaba la misma mala ubicación → decenas de ellas
Corrección permanente
Cuerpo fuera del docroot + solo public/ enlazado simbólicamente

Los peores archivos son .env y .git

.env reúne todos los secretos en un solo archivo: autenticación de BD, correo, API externas, la clave de cifrado de la app (APP_KEY). Con .git público, los atacantes recuperan no solo los valores actuales sino cada rotación pasada recorriendo el historial de git. Véase qué es .env.

Qué estaba pasando

En el host compartido, muchas apps Laravel estaban dispuestas así. Aquí están las disposiciones peligrosa y correcta una al lado de la otra:

Disposición peligrosa (común)

~/public_html/
└── app/          ← todo Laravel (incorrecto)
    ├── .env      ← legible en /app/.env
    ├── .git/     ← clonar restaura el historial completo
    ├── vendor/  config/  storage/
    └── public/   ← lo ÚNICO que debería estar aquí

Disposición correcta

~/laravel/app/    ← cuerpo fuera de la raíz web
├── .env  .git/   ← inalcanzable por HTTP = seguro
└── public/       ← solo esto, expuesto vía enlace simbólico
        └── index.php

Archivos típicos que devolvían 200 OK al mundo exterior:

ArchivoPor qué es peligroso
.env / .env.bk_*autenticación de BD, correo, claves de API externas, APP_KEY — todo en texto plano
el directorio .git/git clone restaura el código fuente y el historial completo
composer.lock / package-lock.jsonversiones exactas de dependencias = indica a los atacantes qué CVE conocidos aplican
credentials.jsonsecretos de cliente OAuth, empaquetados
*.sql / db-*.sql.gzun volcado completo de la base de datos, decenas de MB comprimidos

Lo más peligroso: el mismo error se extiende

Una vez que lo dispones así, cada nueva app del mismo tipo copia la misma mala ubicación. Una mala configuración se vuelve decenas. La regla: en el momento en que lo notes, inspecciónalas todas.

La corrección en tres pasos

Paso 1 — Medida de urgencia: bloquea los archivos sensibles a nivel HTTP

Primero, detén «que se filtre más». Es reversible y no afecta a las páginas normales. Añade un bloque deny al principio de public_html/.htaccess (un ejemplo de configuración defensiva):

# === SECURITY BLOCK ===
# 404 a cualquier cosa bajo .env / .git
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
 
# denegar copias de seguridad, volcados, archivos de credenciales
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
    Require all denied
</FilesMatch>
 
# si las internas de Laravel están visibles en /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

Pero una lista negra necesita una regla nueva para cada archivo nuevo, así que siempre se escapa algo. Olvidarás composer.lock; los nombres de archivo nuevos no quedarán cubiertos. Trátalo como una medida temporal, y haz la corrección duradera en el Paso 3.

Paso 2 — Rotar claves (asume que las vieron)

Incluso tras bloquear con .htaccess, trata los secretos como ya vistos. Por orden de prioridad:

  1. Claves de API externas (máxima prioridad, inmediato): se asocian a facturación y toma de control de cuentas. Revoca + reemite el CLIENT_SECRET de OAuth, las claves de API y las credenciales de la nube, con alcance de mínimo privilegio.
  2. Secretos de cliente OAuth.
  3. APP_KEY: regenerarlo puede romper sesiones y dejar indescifrables columnas de BD cifradas — comprueba primero a qué afecta.
  4. Correo/SMTP, credenciales de BD.

Un error de rotación que ocurrió de verdad

Con algunos proveedores OAuth, reemitir el secreto de cliente también invalida los refresh_token existentes — el refresco con tokens guardados falla y provoca una caída de producción. Ten siempre un flujo de reautorización listo antes de reemitir. Y actualiza también tu .env / configuración local, o el siguiente despliegue reintroduce el valor antiguo.

Paso 3 — Reestructurar: mover el cuerpo fuera del docroot (la corrección de fondo)

Vuelve a disponer cada app así:

# 1. Mover el cuerpo fuera de la raíz web (mismo sistema de archivos → mv es instantáneo, atómico)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. Poner solo un bootstrap mínimo en la raíz web
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
# 3. Copiar public/.htaccess de Laravel, enlazar simbólicamente los activos estáticos, limpiar la caché de config

En esta forma, .env, .git y vendor/ simplemente no existen en la raíz web, así que estás a salvo sin depender de reglas de .htaccess. Los pasos completos están en mantener .env fuera de la web pública en hosting compartido.

✗ Peligroso: cuerpo dentro de la raíz web

— frontera web (legible por HTTP) —

.env · .git · vendor · public

↑ todo dentro de la línea = cualquiera puede leerlo

✓ Seguro: cuerpo fuera de la raíz web

— frontera web (legible por HTTP) —

solo public/ (enlace simbólico)

.env · .git están fuera = inalcanzables

Una sola cosa lo decide: dentro o fuera de la raíz web. Si .env y .git están dentro, cualquiera puede leerlos por HTTP.

Problemas que encontramos al corregirlo

  • Problema de alcance de subdominio: reapuntar el docroot de un subdominio registrado a otro árbol vía enlace simbólico puede no seguirse y devolver 500. → Mantén el docroot como un directorio real y haz require por ruta absoluta desde un pequeño index.php dentro de él (la forma «bootstrap-redirect») para estabilidad.
  • opcache recuerda un estado roto: opcache puede memorizar una carga fallida de index.php y seguir devolviendo 500 incluso tras reconstruir. Último recurso: cambia el nombre del archivo (entry.php, etc.).
  • Un docroot de subdominio «no registrado» legible vía el padre: https://sub.example/ puede ser inválido mientras https://example/sub/ aún muestra el contenido — una ruta fácil de pasar por alto. Al inspeccionar, bloquea también la ruta del dominio padre.
  • Secretos incrustados en config/*.php: combinado con un .git público, el historial revela cada rotación. Pasa siempre por env() y nunca pongas valores reales en los defaults de respaldo.

Lección: la disposición por defecto del servidor facilita este error

La práctica temprana del hosting compartido ponía todo directamente bajo public_html. Cuando Laravel (que espera exponer solo public/) se extendió allí, una disposición peligrosa de «proyecto bajo public_html/<app>/, exponer su public/» se convirtió en el estándar de facto — la propia documentación de los proveedores usa ejemplos basados en public_html, lo que ayudó a extenderla. Así que corrígelo con proceso, no con vigilancia:

Previene la recurrencia con proceso

  • Haz que el valor por defecto del despliegue sea «cuerpo fuera del docroot, solo public/ enlazado simbólicamente».
  • Comprobaciones de CI/lint: nunca hacer commit de .env; nunca colocar un proyecto directamente bajo public_html/<app>.
  • Autocomprobación tras el despliegue: confirma que /.env y /.git/config no se pueden obtener desde fuera.

Este sitio integra este hábito de «inspecciona tu propio sitio tú mismo» en su itinerario de aprendizaje. → qué es lo peligroso de .env y las claves de API

Leer a continuación

FAQ

QMi .env está expuesto — ¿qué hago primero?
A

Primero bloquea la ruta vía .htaccess, luego rota las claves de API externas y los secretos OAuth del .env por orden de prioridad (asume que los vieron), y después mueve el cuerpo de la app fuera de la raíz web para estar a salvo sin depender de reglas.

Q¿Por qué es peligroso poner Laravel bajo public_html?
A

Laravel está hecho para exponer solo public/. Pon todo el proyecto en el docroot y todo lo que hay por encima — .env (todos tus secretos), .git (historial recuperable), vendor/ — se vuelve legible por HTTP.

Q¿Basta con bloquear con una lista negra en .htaccess?
A

Funciona como medida temporal, pero cada nuevo nombre de archivo necesita una regla nueva, así que siempre se escapa algo. La corrección duradera es el cambio estructural: pon el cuerpo de la app fuera del docroot y expón solo public/ mediante un enlace simbólico.