Para: cualquiera que ejecute apps tipo Laravel (donde solo public/ debería estar expuesto) en hosting compartido con Apache/.htaccess. Destilado de un incidente real (→ .env expuesto al mundo) — para asegurar tu propio despliegue.
La visión de este sitio: esto no es 'el error de una persona'
La cultura temprana del hosting compartido ponía todo directamente bajo public_html. Laravel (que espera que solo se exponga public/) fluyó hacia eso, y un diseño peligroso se convirtió en el estándar de facto — incluso la propia documentación de los proveedores pone los proyectos dentro de public_html. Así que la solución es proceso, no vigilancia: cambia tu valor por defecto de despliegue.
Acierta con la ubicación (diagrama)
✗ Peligroso (cuerpo en la raíz pública)
public_html/ ├─ .env ← ¡legible! ├─ .git/ vendor/ └─ public/
✓ Seguro (cuerpo fuera, solo public)
app-laravel/ ← fuera de la raíz web ├─ .env .git/ ← inalcanzables └─ public/ → requerido desde public_html
Qué solución encaja con tu entorno
"Mantener el .env fuera de la web" se hace de forma distinta según el tipo de servidor y tus permisos. Busca primero tu fila y después sigue los pasos de abajo.
| Entorno | Solución permanente | Primeros auxilios | Cuidado con |
|---|---|---|---|
| Hosting compartido (con SSH) | Saca el cuerpo de la app de la carpeta pública y deja allí solo un index.php pequeño (Paso 2) | Denegar en .htaccess | Convertir la propia carpeta pública en un symlink devuelve 500 en algunos entornos |
| Hosting compartido (solo FTP) | Vuelve a subir el cuerpo de la app un nivel por encima de la carpeta pública (public_html, etc.) con tu cliente FTP | Denegar en .htaccess | La mayoría de los hostings compartidos permiten escribir por encima de la carpeta pública; si el tuyo no, pregunta al soporte |
| El panel de control permite elegir la carpeta pública | Apunta la carpeta pública directamente a app-laravel/public | No hace falta (pero compruébalo) | Tras el cambio, borra cualquier copia de la app que quede en la carpeta pública antigua |
| VPS (Apache) | Apunta el DocumentRoot del VirtualHost a public/ | Reglas de denegación en la configuración del servidor | Con AllowOverride None (el valor por defecto en Apache 2.4), el .htaccess no se lee |
| VPS (Nginx) | Apunta root a public/ y deniega los archivos que empiezan por punto | Reglas de denegación en la configuración del servidor | Nginx no lee el .htaccess |
El objetivo es el mismo en todos los casos: que no haya .env, .git, vendor/ ni storage/ dentro de la carpeta pública. La documentación oficial de Laravel también advierte que mover index.php a la raíz del proyecto y servir desde ahí expone muchos archivos de configuración sensibles a internet.
Paso 1: Primeros auxilios (rápido, reversible)
Añade un bloque de denegación al principio del .htaccess de la raíz web (un ejemplo de configuración defensiva). Las páginas normales no se ven afectadas.
# === SECURITY BLOCK ===
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
Require all denied
</FilesMatch>
<FilesMatch "(?i)^(phpinfo|info)\.php$">
Require all denied
</FilesMatch>
# if Laravel internals are visible at /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===Los primeros auxilios no son el objetivo
Una lista negra siempre deja algo fuera: composer.lock o cualquier nombre de archivo nuevo. Trátalo como ganar tiempo hasta el Paso 2.
Paso 2: Reestructurar (la solución permanente)
Mueve el cuerpo de la app fuera de la raíz web y deja un punto de entrada diminuto que solo lo cargue.
# 1. Move the body outside the web root (same filesystem → mv is instant, atomic)
mv ~/example/public_html/app ~/example/app-laravel
# 2. Put only a minimal entry in the web root
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
# 3. Copy Laravel public/.htaccess, symlink static assets
# 4. Clear config cache
rm -f ~/example/app-laravel/bootstrap/cache/{config,services,packages,routes-v7}.phpUna lección de operar este sitio: bootstrap-redirect en lugar de symlink
Hacer del propio public_html un symlink a public/ puede devolver 500 en el docroot de un subdominio registrado en algunos entornos (no se sigue). Mantener el docroot como un directorio real y hacer require por ruta absoluta desde un pequeño index.php (la forma "bootstrap-redirect") es más estable en hosting compartido con alcance restringido. Además, opcache puede memorizar una carga fallida y seguir devolviendo 500 — el último recurso es cambiar el nombre del archivo de entrada (entry.php).
Paso 3: Rota las claves (asume que fueron vistas)
Si hubo cualquier ventana de exposición, trata las claves del .env como ya vistas y reemplázalas. Orden: secretos de API externa / OAuth → claves de cifrado → correo → BD. Ver el glosario del .env. Reemitir un secreto de OAuth puede invalidar los refresh_token, así que ten lista primero una vía de reautorización.
Paso 4: Autocomprobación (hazlo un hábito)
Por último, confirma que nada esté de verdad expuesto en tu propio sitio (solo contra dominios que poseas).
# A 200 with a body means it's exposed. 403/404 is okay for now.
curl -sI https://your-domain/.env | head -1
curl -sI https://your-domain/.git/config | head -1Comprobar esto en cada despliegue detecta pronto los errores de ubicación.
Cómo comprobar tu propia configuración (más a fondo)
Además de las dos líneas del Paso 4, estas comprobaciones detectan más de lo que suele pasarse por alto. Ejecútalas solo contra tu propio servidor y tus propios dominios.
Mira el contenido, no solo el código de estado
Algunas apps responden a cualquier URL desconocida con la página de inicio y un 200, así que el código de estado por sí solo no dice nada. Muestra solo el principio, por ejemplo curl -s https://your-domain/.env | head -c 120, y mira si aparecen líneas de configuración como APP_KEY= o DB_PASSWORD=. En .git/HEAD, una línea que empieza por ref: refs/heads/ significa que está expuesto.
Comprueba todas las apps, no solo una
Repite la comprobación para cada app en un subdominio o subdirectorio, no solo el dominio principal. Los errores de ubicación suelen copiarse al añadir apps, así que arreglar una no significa que las demás estén arregladas.
Busca en la carpeta pública del servidor
Con SSH, busca directamente los archivos peligrosos, por ejemplo find ~/public_html -maxdepth 4 \( -name ".env*" -o -name ".git" -o -name "*.sql" -o -name "*.zip" \). Que no salga nada es el resultado que quieres. Si solo tienes FTP, activa "mostrar archivos ocultos" en tu cliente FTP y revisa el listado.
Confirma que el .htaccess surte efecto
Pon en la carpeta pública un archivo de prueba inofensivo y sin secretos (por ejemplo check.bak con solo test dentro) y ábrelo por URL. Deberías recibir un 403. Si se abre, el .htaccess no se está leyendo, así que revisa el tipo de servidor (Nginx, por ejemplo) o su configuración. Borra el archivo de prueba al terminar.
Asegúrate de que el modo debug está desactivado
En el .env, APP_ENV debe ser production y APP_DEBUG debe ser false. La documentación de Laravel dice que APP_DEBUG=true en producción conlleva el riesgo de exponer valores de configuración sensibles a tus usuarios. Con SSH, php artisan about --only=environment muestra lo mismo.
Errores frecuentes y cómo corregirlos
- Borrar el .env pero dejar el .git: un
.gitexpuesto puede permitir reconstruir commits antiguos, incluido un.envo claves antiguas. Saca también.gitde la carpeta pública y reemite cualquier secreto que se haya subido alguna vez en un commit. - Dejar copias como
.env.bako.env.olden la carpeta pública: una copia con valores reales es tan sensible como el.env. Sácala o bórrala. Deja solo valores de ejemplo en.env.example. - Subir todo el proyecto como ZIP y dejarlo tras descomprimirlo: el ZIP contiene el
.env. Bórralo justo después de descomprimir. - Ejecutar producción con
APP_DEBUG=true: las páginas de error pueden revelar valores de configuración. Ponlo siempre enfalseen producción. - Creer que estás a salvo porque escribiste el
.htaccess: las configuraciones con Nginx o conAllowOverride Nonenunca lo leen. Usa la comprobación con el archivo de prueba de arriba para confirmarlo. - Cambiar el
APP_KEYde golpe: según la documentación de Laravel, cambiar la clave cierra la sesión de todos los usuarios y hace que los datos cifrados con la clave antigua ya no se puedan descifrar. En Laravel 11 y posteriores, pon la clave antigua enAPP_PREVIOUS_KEYSantes de cambiarla.
Un caso real: archivos .env expuestos recogidos a gran escala
En agosto de 2024, el equipo de investigación de una empresa de seguridad informó de una operación que recogía automáticamente archivos .env expuestos en servidores web, usaba las credenciales de la nube que contenían para copiar datos y después exigía un rescate. Según el informe, se apuntó a unos 110.000 dominios y se recogieron más de 90.000 variables de entorno, de las que unas 7.000 eran credenciales de servicios en la nube y unas 1.500 eran credenciales de cuentas de redes sociales.
El informe no atribuye la causa a una vulnerabilidad del lado de la nube, sino a errores de configuración de las organizaciones afectadas, que habían expuesto sus archivos .env por error. De ahí salen dos lecciones:
- Los
.envexpuestos no se encuentran por casualidad; hay programas que los buscan a gran escala - Un solo
.envpuede extender el daño a otros servicios, como el almacenamiento en la nube o el correo
No des por hecho que nadie encontrará un sitio pequeño. Un caso anonimizado del mismo error de ubicación está en El .env expuesto al mundo entero.
Sigue leyendo
- Incidente: El .env expuesto al mundo entero
- Glosario: qué es .env
- Fundamentos: qué tiene de peligroso el .env y las claves de API
- Cuando el proveedor sufre una brecha: qué hacer cuando tu hosting web está comprometido (en inglés; el caso que tu propia configuración no puede evitar)
- Comprobar una intrusión: cómo saber si han hackeado tu sitio web o tu servidor (dónde mirar si resulta que tu .env estuvo público)
Fuentes
- Documentación de Laravel, "Deployment" (dirigir las peticiones a
public/index.php, no servir nunca desde la raíz del proyecto,APP_DEBUG=falseen producción): laravel.com - Documentación de Laravel, "Configuration" (no subir el
.enval control de versiones; Debug Mode): laravel.com - Documentación de Laravel, "Encryption" (rotación del
APP_KEYyAPP_PREVIOUS_KEYS): laravel.com - Apache HTTP Server 2.4, "core - AllowOverride" (el
.htaccessno se lee cuando valeNone; el valor por defecto esNone): httpd.apache.org - Apache HTTP Server 2.4, ".htaccess files" (cuándo usarlos y precauciones): httpd.apache.org
- OWASP, "WSTG-CONF-04 Review Old Backup and Unreferenced Files for Sensitive Information": owasp.org
- Unit 42 (Palo Alto Networks), "Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments" (15 de agosto de 2024): unit42.paloaltonetworks.com