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).
- 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.phpArchivos típicos que devolvían 200 OK al mundo exterior:
| Archivo | Por 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.json | versiones exactas de dependencias = indica a los atacantes qué CVE conocidos aplican |
credentials.json | secretos de cliente OAuth, empaquetados |
*.sql / db-*.sql.gz | un 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:
- Claves de API externas (máxima prioridad, inmediato): se asocian a facturación y toma de control de cuentas. Revoca + reemite el
CLIENT_SECRETde OAuth, las claves de API y las credenciales de la nube, con alcance de mínimo privilegio. - Secretos de cliente OAuth.
APP_KEY: regenerarlo puede romper sesiones y dejar indescifrables columnas de BD cifradas — comprueba primero a qué afecta.- 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 configEn 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
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
requirepor ruta absoluta desde un pequeñoindex.phpdentro de él (la forma «bootstrap-redirect») para estabilidad. - opcache recuerda un estado roto: opcache puede memorizar una carga fallida de
index.phpy 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 mientrashttps://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.gitpúblico, el historial revela cada rotación. Pasa siempre porenv()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 bajopublic_html/<app>. - Autocomprobación tras el despliegue: confirma que
/.envy/.git/configno 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
- Glosario: qué es .env · qué es un CVE
- Defensa: mantener .env fuera de la web pública en hosting compartido
- Framework: seguridad de Laravel (los tres grandes fallos: .env, APP_DEBUG, autorización)
- Incidente: cuando una clave de API robada se facturó de forma fraudulenta (la otra vía de fuga — en tiempo de ejecución)