Guias de Segurança
O .env de apps Laravel era legível pelo mundo inteiro — o erro mais comum de hospedagem compartilhada
Em hospedagem compartilhada, apps Laravel acabam com .env, .git, dumps de DB e segredos OAuth legíveis por HTTP. A causa: o projeto inteiro sob a raiz web em vez de só o public/. A correção em três passos e por que é um padrão ruim do setor inteiro.
Uma configuração incorreta que realmente acontece em hospedagem compartilhada, com domínios e valores removidos e transformada em lição. Isto não é sobre encontrar os sites de outras pessoas — é sobre proteger seu próprio deploy (ou um herdado).
- Classe
- Exposição de .env / .git / dumps de DB (configuração incorreta)
- Severidade
- Crítica (todo segredo legível por HTTP)
- Causa
- Corpo do Laravel colocado diretamente sob o public_html (só o public/ deveria ser exposto)
- Propagação
- Cada novo app copiava o mesmo buraco → dezenas deles
- Correção permanente
- Corpo fora da docroot + só o public/ com symlink
Os piores arquivos são .env e .git
O .env é um chaveiro: autenticação de DB, e-mail, APIs externas, a chave de criptografia do app (APP_KEY). Com o .git público, invasores recuperam não só os valores atuais, mas cada rotação passada percorrendo o histórico do git. Veja O que é o .env.
O que estava acontecendo
No host compartilhado, muitos apps Laravel estavam dispostos assim. Eis os layouts perigoso e correto lado a lado:
Layout perigoso (comum)
~/public_html/
└── app/ ← Laravel inteiro (errado)
├── .env ← legível em /app/.env
├── .git/ ← clone restaura o histórico completo
├── vendor/ config/ storage/
└── public/ ← a ÚNICA coisa que deveria estar aquiLayout correto
~/laravel/app/ ← corpo fora da raiz web
├── .env .git/ ← inacessível por HTTP = seguro
└── public/ ← só este, exposto via symlink
└── index.phpArquivos típicos que retornavam 200 OK para o mundo externo:
| Arquivo | Por que é perigoso |
|---|---|
.env / .env.bk_* | autenticação de DB, e-mail, chaves de API externas, APP_KEY — tudo em texto puro |
o diretório .git/ | git clone restaura o código-fonte e o histórico completo |
composer.lock / package-lock.json | versões exatas de dependências = uma lista de alvos com CVE conhecido |
credentials.json | segredos de cliente OAuth, empacotados |
*.sql / db-*.sql.gz | um dump completo do banco de dados, dezenas de MB compactados |
A parte assustadora: a propagação
Uma vez disposto assim, cada novo app do mesmo tipo copia o mesmo buraco. Uma configuração incorreta vira dezenas. A regra: no momento em que você notar, inspecione todos.
A correção em três passos
Passo 1 — Primeiros socorros: bloqueie arquivos sensíveis no nível do HTTP
Primeiro, pare "mais vazamento". É reversível e não afeta as páginas normais. Adicione um bloco de deny no topo do public_html/.htaccess (um exemplo de configuração defensiva):
# === SECURITY BLOCK ===
# 404 anything under .env / .git
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
# deny backups, dumps, credential files
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
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 ===Mas uma blacklist é fundamentalmente enxugar gelo. Você vai esquecer o composer.lock; novos nomes de arquivo adicionam novos buracos. Trate-a como primeiros socorros de melhor esforço e coloque a correção real no Passo 3.
Passo 2 — Rotacione as chaves (suponha que foram vistas)
Mesmo depois de bloquear com o .htaccess, trate os segredos como já vistos. Em ordem de prioridade:
- Chaves de API externas (prioridade máxima, imediato): elas mapeiam para cobrança e tomada de conta. Revogue + reemita o
CLIENT_SECRETOAuth, chaves de API e credenciais de nuvem, com escopo de menor privilégio. - Segredos de cliente OAuth.
APP_KEY: regenerar pode quebrar sessões e tornar colunas de DB criptografadas indecifráveis — verifique o raio de explosão primeiro.- E-mail/SMTP, credenciais de DB.
Uma armadilha de rotação (de fato encontrada em campo)
Com alguns provedores OAuth, reemitir o segredo de cliente também invalida os refresh_tokens existentes — a renovação com tokens armazenados falha e causa uma queda em produção. Sempre tenha um fluxo de reautorização pronto antes de reemitir. E atualize seu .env / config local também, ou o próximo deploy reintroduz o valor antigo.
Passo 3 — Reestruture: mova o corpo para fora da docroot (a correção real)
Redisponha cada app assim:
# 1. Mova o corpo para fora da raiz web (mesmo sistema de arquivos → mv é instantâneo, atômico)
mv ~/example/public_html/app ~/example/app-laravel
# 2. Coloque só um bootstrap mínimo na raiz 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. Copie o public/.htaccess do Laravel, faça symlink dos assets estáticos, limpe o cache de configNesta forma, .env, .git e vendor/ simplesmente não existem na raiz web, então você fica seguro sem depender de regras do .htaccess. Os passos completos estão em Mantendo o .env fora da web pública em hospedagem compartilhada.
✗ Perigoso: corpo dentro da raiz web
— fronteira web (legível por HTTP) —
.env · .git · vendor · public
↑ tudo dentro da linha = totalmente exposto
✓ Seguro: corpo fora da raiz web
— fronteira web (legível por HTTP) —
só public/ (symlink)
.env · .git estão fora = inacessíveis
Armadilhas "que não estão no manual"
- Problema de escopo de subdomínio: repontar a docroot de um subdomínio registrado para outra árvore via symlink pode não seguir e retornar 500. → Mantenha a docroot um diretório real e faça
requirepor caminho absoluto a partir de um pequenoindex.phpdentro dele (a forma "bootstrap-redirect") para estabilidade. - opcache lembra de um estado quebrado: o opcache pode memorizar uma falha de carga do
index.phpe seguir retornando 500 mesmo depois de você reconstruir. Último recurso: troque o nome do arquivo (entry.php, etc.). - Uma docroot de subdomínio "não registrado" legível pelo pai:
https://sub.example/pode ser inválido enquantohttps://example/sub/ainda mostra o conteúdo — uma entrada facilmente esquecida. Ao inspecionar, bloqueie também o caminho do domínio-pai. - Segredos hardcoded em
config/*.php: combinados com um.gitpúblico, o histórico revela cada rotação. Sempre passe porenv()e nunca coloque valores reais em defaults de fallback.
A maior lição: isto não é "o erro de uma pessoa"
A cultura inicial de hospedagem compartilhada colocava tudo diretamente sob o public_html. O Laravel (que espera só o public/ exposto) entrou nisso, e um layout perigoso de "projeto sob public_html/<app>/, expor seu public/" virou o padrão de fato — a própria documentação dos fornecedores usa exemplos baseados em public_html, o que piora. Então corrija com processo, não vigilância:
Previna a recorrência com processo
- Faça o padrão de deploy ser "corpo fora da docroot, só o
public/com symlink". - Verificações de CI/lint: nunca comitar
.env; nunca colocar um projeto diretamente sobpublic_html/<app>. - Autoverificação após deploy: confirme que
/.enve/.git/confignão podem ser obtidos de fora.
Este site incorpora esse hábito de "inspecione seu próprio site você mesmo" em sua trilha de aprendizado. → O que é perigoso em .env e chaves de API
Leia a seguir
- Glossário: O que é o .env · O que é um CVE
- Defesa: Mantendo o .env fora da web pública em hospedagem compartilhada
- Incidente: Quando uma chave de API roubada foi cobrada por fraude (o outro caminho de vazamento — em tempo de execução)
FAQ
QMeu .env está exposto — o que faço primeiro?
Estanque o sangramento (bloqueie o caminho via .htaccess), depois rotacione as chaves de API externas e segredos OAuth no .env em ordem de prioridade (suponha que foram vistos), depois mova o corpo do app para fora da raiz web para ficar seguro sem depender de regras.
QPor que colocar o Laravel sob o public_html é perigoso?
O Laravel foi feito para expor só o public/. Coloque o projeto inteiro na docroot e tudo acima dele — .env (todos os seus segredos), .git (histórico recuperável), vendor/ — fica legível por HTTP.
QBloquear com uma blacklist no .htaccess é suficiente?
Funciona como primeiros socorros, mas é fundamentalmente enxugar gelo — cada novo nome de arquivo adiciona um buraco. A correção real é a mudança estrutural: coloque o corpo do app fora da docroot e exponha só o public/ via symlink.