A misconfiguration that really happens on shared hosting, with domains and values removed and turned into a lesson. This is not about finding other people's sites — it's about securing your own (or an inherited) deployment.
- Class
- Exposure of .env / .git / DB dumps (misconfiguration)
- Severity
- Critical (every secret readable over HTTP)
- Cause
- Laravel body placed directly under public_html (only public/ should be exposed)
- Propagation
- Each new app copied the same misplacement → dozens of them
- Permanent fix
- Body outside the docroot + only public/ symlinked
The worst files are .env and .git
.env holds every secret in one file: DB auth, mail, external APIs, the app encryption key (APP_KEY). With .git public, attackers recover not just current values but every past rotation by walking the git history. See What is .env.
What was happening
On the shared host, many Laravel apps were laid out this way. Here are the dangerous and correct layouts side by side:
Dangerous layout (common)
~/public_html/
└── app/ ← whole Laravel (wrong)
├── .env ← readable at /app/.env
├── .git/ ← clone restores full history
├── vendor/ config/ storage/
└── public/ ← the ONLY thing meant to be hereCorrect layout
~/laravel/app/ ← body outside the web root
├── .env .git/ ← unreachable over HTTP = safe
└── public/ ← only this, exposed via symlink
└── index.phpTypical files that returned 200 OK to the outside world:
| File | Why it's dangerous |
|---|---|
.env / .env.bk_* | DB auth, mail, external API keys, APP_KEY — all in plaintext |
the .git/ directory | git clone restores source and full history |
composer.lock / package-lock.json | exact dependency versions = tells attackers which known CVEs apply |
credentials.json | OAuth client secrets, bundled |
*.sql / db-*.sql.gz | a full database dump, tens of MB compressed |
The most dangerous part: the same mistake spreads
Once you lay it out this way, each new app of the same kind copies the same misplacement. One misconfiguration becomes dozens. The rule: the moment you notice, inspect them all.
The three-step fix
Step 1 — First aid: block sensitive files at the HTTP level
First, stop "more leaking." It's reversible and doesn't affect normal pages. Add a deny block at the top of public_html/.htaccess (a defensive config example):
# === 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 ===But a blacklist needs a new rule for every new file, so something always gets missed. You'll forget composer.lock; new filenames won't be covered. Treat it as a temporary measure, and make the lasting fix in Step 3.
Step 2 — Rotate keys (assume they were seen)
Even after blocking with .htaccess, treat the secrets as already seen. In priority order:
- External API keys (top priority, immediate): they map to billing and account takeover. Revoke + re-issue OAuth
CLIENT_SECRET, API keys, and cloud credentials, scoped to least privilege. - OAuth client secrets.
APP_KEY: regenerating can break sessions and make encrypted DB columns undecryptable — check what it affects first.- Mail/SMTP, DB credentials.
A rotation mistake that actually happened
With some OAuth providers, re-issuing the client secret also invalidates existing refresh_tokens — refresh with stored tokens fails and causes a production outage. Always have a re-authorization flow ready before re-issuing. And update your local .env / config too, or the next deploy reintroduces the old value.
Step 3 — Restructure: move the body outside the docroot (the root fix)
Re-lay each app like this:
# 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 bootstrap 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, clear config cacheIn this shape, .env, .git, and vendor/ simply don't exist in the web root, so you're safe without relying on .htaccess rules. Full steps are in Keeping .env off the public web on shared hosting.
✗ Dangerous: body inside the web root
— web boundary (readable over HTTP) —
.env · .git · vendor · public
↑ all inside the line = anyone can read them
✓ Safe: body outside the web root
— web boundary (readable over HTTP) —
public/ only (symlink)
.env · .git are outside = unreachable
Problems we hit while fixing it
- Subdomain scope problem: re-pointing a registered subdomain's docroot to another tree via symlink may not follow and returns 500. → Keep the docroot a real directory and
requireby absolute path from a smallindex.phpinside it (the "bootstrap-redirect" shape) for stability. - opcache remembers a broken state: opcache can memorize a failed
index.phpload and keep returning 500 even after you rebuild. Last resort: change the filename (entry.php, etc.). - An "unregistered" subdomain docroot readable via the parent:
https://sub.example/may be invalid whilehttps://example/sub/still shows the contents — an easily missed path. When inspecting, block the parent-domain path too. - Hardcoded secrets in
config/*.php: combined with a public.git, history reveals every rotation. Always go throughenv()and never put real values in fallback defaults.
Lesson: the default server layout makes this mistake easy
Early shared-hosting practice put everything directly under public_html. When Laravel (which expects only public/ exposed) came into wide use there, and a dangerous "project under public_html/<app>/, expose its public/" layout became the de-facto standard — vendors' own docs use public_html-based examples, which helped spread it. So fix it with process, not vigilance:
Prevent recurrence with process
- Make the deployment default "body outside the docroot, only
public/symlinked". - CI/lint checks: never commit
.env; never place a project directly underpublic_html/<app>. - Self-check after deploy: confirm
/.envand/.git/configaren't fetchable from the outside.
This site builds this "inspect your own site yourself" habit into its learning track. → What's dangerous about .env and API keys
Read next
- Glossary: What is .env · What is a CVE
- Defense: Keeping .env off the public web on shared hosting
- Incident: When a stolen API key got billed for fraud (the other leak path — runtime)
- By framework: Laravel security (.env exposure, APP_DEBUG, authorization)