Committing PHP config files to Git is the most common security mistake developers make. Learn why it's dangerous, how to fix it, and what to do if it's already too late.
In 2023, a developer pushed a config file to a public GitHub repo. Twelve minutes later, someone had spun up $50,000 worth of AWS compute using his credentials.
He didn't know about it until the bill arrived.
This isn't a horror story from a careless intern at a fly-by-night startup. This is a pattern so common that GitGuardian — a company that does nothing but scan for exposed secrets — found over 10 million secrets leaked on GitHub in a single year. That's not a typo. Ten million. In one year. On one platform.
And the frustrating part? The fix takes about 45 seconds.
So why does this keep happening? Because the bad habit forms on day one, it feels harmless, and nobody corrects it until it's too late.
TL;DR Speedrun
- The file you should never commit is your PHP configuration file — usually
config.php,db.php,wp-config.php, or anything that stores credentials, API keys, or environment variables. - Thousands of these files are exposed on GitHub right now, searchable by anyone using GitHub's built-in search.
- Git doesn't forget. Even if you delete the file, the secret lives in your commit history forever unless you rewrite it.
- The fix: use
.envfiles +phpdotenv, add them to.gitignorebefore your first commit, and use environment variables in production. - If you've already committed secrets, rotate them immediately — assume they're compromised.
What You'll Learn
- Which PHP files are most commonly — and dangerously — committed to Git
- Why Git's history makes "just delete it" not good enough
- How to restructure your project so secrets never touch version control
- What to do if you've already made the mistake (most of us have)
- How to audit your own repos in under 10 minutes
- How to build automated guardrails so this never happens again
The File Everyone Commits (Without Realizing It)
Ask a PHP beginner to connect to a database and they'll write something like this:
<?php
$host = 'localhost';
$db = 'my_app';
$user = 'root';
$pass = 'supersecretpassword123';
$conn = new PDO("mysql:host=$host;dbname=$db", $user, $pass);They save it as config.php or db.php. They git add ., git commit -m "initial commit", git push.
Done. App works. Ship it.
And that's when the clock starts ticking.
It doesn't matter if the repo is private right now. Repos get made public by accident. Companies get acquired and visibility settings change. Developers fork projects without thinking. And if you're using a free GitHub plan, your settings might not be what you think they are.
More commonly though? People do it on purpose — because the repo is "just for learning" or "just a side project" and they never imagine it'll matter.
But "just a side project" often runs on the same database as the production site. The same Stripe account. The same Mailgun API key that controls your entire mailing list. Context doesn't shrink the blast radius.
It always matters.
Why This Habit Forms in the First Place
Let's be honest about where this comes from. Most PHP developers learn from tutorials. And most tutorials — especially older ones — are absolutely terrible about secrets hygiene.
A YouTube tutorial or a blog post shows you how to connect to MySQL. It says:
"Create a file called
config.phpand add your database credentials."
And then it moves on. No mention of .gitignore. No mention of environment variables. No warning about what happens when you push this to GitHub. Just: "Here's the file, now let's build the app."
You follow along. It works. You feel great. You push to GitHub because that's what developers do. And you've just set a habit that'll follow you for years.
The problem isn't that beginners make this mistake. The problem is that the ecosystem has been slow to call it out. Stack Overflow answers from 2012 still show up at the top of Google and suggest hardcoding credentials. Tutorials assume you know better. And "you should know better" is not a security policy.
So let's fix it from the root.
The "It's Private" Trap
Here's the thing about private repositories: they're only as secure as your account.
Weak password? Compromised email? A third-party app with repo access that got breached? Now your "private" repo is someone else's problem — and your credentials are their treasure.
GitHub itself has had security incidents. So have GitLab, Bitbucket, and every major hosting platform. Privacy is not security. It's just obscurity with a membership fee.
And then there's the collaborator problem. Every developer you add to a private repo can clone it locally, keep that clone forever, and do whatever they want with it after they leave the team. You can't un-share something once it's been shared. Offboarding a developer doesn't reach into their laptop and delete their local clone.
Think about every freelancer you've ever brought onto a project. Every contractor. Every junior dev who helped debug a weekend issue. All of them potentially have a copy of your repo — and everything that was ever in it.
The .env pattern exists precisely because of this. It separates code (which is fine to share) from configuration (which is not). Code is the blueprint. Configuration is the key to the building. You can hand out blueprints. You don't hand out keys.
Git's Dark Secret: History Is Forever
Let's say you've already pushed your config.php and you just realized it. You delete the file. You push again. Crisis averted?
No. Absolutely not.
Git is a time machine. Every commit is a snapshot. When you delete a file and push, you're adding a new snapshot that doesn't include the file — but every previous snapshot still exists, fully intact, in your repo's history. Git doesn't overwrite. It appends.
Here's what that means in practice: anyone who runs git clone on your repo — even after you've deleted the file — gets the full history. They can run git log to find the commit where the file existed, then git show <commit-hash>:config.php to read it in full. This takes about 30 seconds if you know what you're doing. Attackers know what they're doing.
To actually remove a secret from Git history, you need to rewrite the history itself using tools like git filter-repo or BFG Repo-Cleaner. It's messy, it breaks existing clones, it confuses collaborators, and it still doesn't help if someone already cloned the repo before you cleaned it up.
That's why the only real solution is: never commit it in the first place.
Think of it like this: writing secrets into a Git commit is like carving your password into wet concrete. You can smooth it over, but anyone who got a photo before you did is holding that password forever.
Which Files Are the Biggest Offenders?
Here are the PHP files most commonly exposed by accident:
wp-config.php — WordPress's main config file. Contains database credentials, secret keys, and salts. Security researchers have documented entire GitHub search queries that surface these files publicly. WordPress powers roughly 43% of the web (W3Techs, 2024) — which means the attack surface is enormous and the payoff for attackers is high.
config.php / database.php — The generic configuration files that beginners create following tutorials. If a tutorial tells you to create this file and doesn't mention .gitignore, it's a bad tutorial. Full stop.
.env (without .gitignore) — Ironic, right? .env files exist to keep secrets safe, but if you commit them without adding them to .gitignore first, you've done exactly what you were trying to avoid. This is more common than you'd think — developers know about .env files, set one up, and then forget the critical last step.
settings.php / app.php — Framework config files (Laravel, Symfony, CodeIgniter) that sometimes get modified with hardcoded credentials when developers are in a hurry. The "I'll clean this up later" commit that never gets cleaned up.
parameters.yml / parameters.php — Symfony's old approach to environment-specific config. Older Symfony projects without proper .gitignore setup are littered with these in public repos.
Old *.sql dump files — Not a PHP file, but almost always committed alongside PHP projects. Full database exports containing real user data, hashed passwords, email addresses, and sometimes even plaintext passwords. These are arguably the worst offender because they expose user data directly, not just access credentials.
The Right Way: Environment Variables + .env
The modern approach separates your code from your configuration. Here's the complete setup:
Step 1: Install vlucas/phpdotenv
composer require vlucas/phpdotenvStep 2: Create your .env file
DB_HOST=localhost
DB_NAME=my_app
DB_USER=root
DB_PASS=supersecretpassword123
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxx
MAIL_API_KEY=SG.xxxxxxxxxxxxxxxxStep 3: Load it in your PHP code
<?php
require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$conn = new PDO(
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']}",
$_ENV['DB_USER'],
$_ENV['DB_PASS']
);Step 4: Add .env to .gitignore immediately — before your first commit
.env
.env.local
.env.*.local
*.sql
*.sqlite
config.phpNotice that last part. While you're in there, add config.php too if you have one. And any database dump files. Make it a habit to be aggressive about what .gitignore covers.
Step 5: Commit a .env.example with placeholder values
DB_HOST=localhost
DB_NAME=your_database
DB_USER=your_user
DB_PASS=your_password
STRIPE_SECRET_KEY=sk_live_your_key_here
MAIL_API_KEY=your_mail_api_keyThis gives collaborators a template without exposing your real secrets. When someone clones your repo, they copy .env.example to .env and fill in the real values. It's clean, it's standard, and every serious PHP framework does this by default.
Step 6 (Production): Use real environment variables, not .env files
In production, you shouldn't be relying on .env files at all. Set environment variables directly through your hosting platform — whether that's Heroku config vars, AWS Parameter Store, a Docker --env-file, or your server's system environment. .env is a development convenience. Production deserves a proper secrets manager.
Real-World Example: The WordPress Breach Pattern
WordPress powers roughly 43% of the web. That's hundreds of millions of sites — and an enormous attack surface.
Security researchers regularly run automated GitHub searches for accidentally exposed wp-config.php files. The pattern is almost always the same: a developer (often a freelancer or agency) sets up a WordPress site locally, initializes a Git repo for version control, and commits everything — including wp-config.php with live database credentials — before pushing to GitHub to share with a client.
They didn't mean to expose it. They just didn't think about it.
In documented cases, attackers who find exposed WordPress credentials have:
- Dumped entire databases including user PII, email addresses, and hashed passwords
- Injected malware or backdoors into theme or plugin files
- Used database access as a pivot point to compromise other sites on shared hosting
- Harvested email lists for spam campaigns and credential stuffing attacks
The attack isn't sophisticated. It's a GitHub search, a clone, and a MySQL client. Anyone with a laptop and an afternoon can do it.
The fix for WordPress is straightforward: move wp-config.php one directory above your web root (WordPress supports this natively), or use the WP Dotenv pattern to load credentials from an .env file. Either way, it never touches your Git history.
How to Audit Your Repos Right Now
You don't have to wait to find out whether you have a problem. Here's how to check in under 10 minutes.
Search your own commit history:
git log -p | grep -i "password\|secret\|api_key\|DB_PASS\|token\|credential"This pipes your entire commit history through grep and highlights any lines that look like credentials. It's not perfect, but it's fast and catches most obvious cases.
Use a dedicated tool:
truffleHog is an open-source tool that scans your Git history for high-entropy strings (how secrets tend to look) and known secret patterns:
pip install trufflehog
trufflehog git file://path/to/your/repoCheck GitHub's own tools:
If your repo is on GitHub, go to Settings → Security → Secret scanning. GitHub will flag known secret patterns automatically and alert you. Enable push protection while you're there — it'll block pushes that contain detected secrets before they even land in your history.
Search GitHub directly:
If you want to check whether your organization's repos have exposed secrets, GitHub's code search supports queries like org:yourorg filename:config.php password. Uncomfortable reading, but better you find it first.
Pitfalls to Avoid
Relying on .gitignore retroactively. Adding a file to .gitignore after you've already committed it does nothing — Git will continue tracking it. You need to run git rm --cached filename to stop tracking it, then commit that change. The file stays on disk; Git just stops watching it.
Using "environment variables" but hardcoding them in deployment scripts. If your deploy.sh, Dockerfile, or CI YAML file contains DB_PASS=supersecret, you've moved the problem without solving it. Those files go into version control too. Use your platform's secret management for anything that runs in a pipeline.
Assuming your framework handles this automatically. Laravel's default .gitignore excludes .env, which is a great start. But some developers see that and assume they're covered — or they manually add .env to a commit to "quickly share the config." Don't manually override your safety net.
Committing to a "temporary" public repo. There's no such thing as a temporary public repo. Screenshots last forever. Automated scrapers run constantly. The Wayback Machine indexes public GitHub repos. Once it's public, assume it's permanent.
Forgetting API keys and third-party tokens. Database passwords get all the attention, but API keys for Stripe, Twilio, SendGrid, or AWS are just as dangerous — often more so, because they grant direct access to paid services with real spend limits. Treat every secret equally.
The "I'll rotate it later" plan. There is no later. The moment you discover an exposed secret, rotation is the first task — not a follow-up ticket, not a note in the README. First task.
Mini Q&A
Q: My repo is private. Do I still need to worry? Yes. Private repos have been breached before, made public accidentally, or cloned by collaborators who later left. Credentials don't belong in any repo, public or private.
Q: I use Laravel. Am I safe by default? Laravel's default .gitignore excludes .env, which is a great start. But if you've modified .gitignore, hardcoded values in config/database.php, or committed .env "just to test," you're not safe. Check your history.
Q: Can GitHub detect and alert me if I commit a secret? Yes — GitHub Secret Scanning detects patterns for major providers (AWS, Stripe, Google Cloud, etc.) and can alert you or block the push. Enable it. But it's not exhaustive — custom credentials and less common services won't always be caught.
Q: What if a third-party plugin or theme hardcodes credentials? That's a different problem, but a serious one. Audit your dependencies. Check reviews. Don't install plugins you can't inspect. And rotate any credentials that ever appear in third-party code you can't fully control.
Q: What's the difference between .env files and environment variables? An .env file is a text file storing key-value pairs, loaded by a library like phpdotenv. Environment variables are values set directly in the OS or container environment. In development, .env is convenient. In production, real environment variables — set by your platform, not a file — are more secure because they never exist as a file that could accidentally be committed or exposed.
Why This Matters Beyond the Obvious
Leaking credentials isn't just a security incident. It cascades.
It's a GDPR incident if user data is exposed through those credentials — which means breach notifications, potential fines, and legal liability depending on your jurisdiction. It's a Terms of Service violation with Stripe, Twilio, and AWS, who will immediately revoke keys found in public repos and may suspend your account. It's an uncomfortable conversation with every client whose data was at risk.
The reputational cost is real too. A GitHub search for a developer's name combined with terms like "password" or "api_key" can surface embarrassing commits from years ago — even on accounts they've since deleted, if someone mirrored the repo before deletion. Future employers do this. Security auditors do this. Bug bounty hunters do this.
Your Git history is a permanent, publicly searchable paper trail. Make sure it's one you'd be comfortable with a future employer, a client, or a security auditor reading line by line.
And here's the thing nobody says out loud: this mistake doesn't disappear from your record just because you cleaned it up. If a breach happens and traces back to a committed credential, the forensics will show when it was committed, when it was removed, and how long the window of exposure was. "I deleted it right away" is not a defense when the window was six months.
Wrap-Up: The Smallest Fix With the Biggest Impact
You don't need a security degree to fix this. You don't need new tooling, a big refactor, or a team meeting.
You need three things:
- A
.envfile for your secrets - That file in
.gitignorebefore your first commit — not after, before - The discipline to never short-circuit that habit "just this once"
The PHP community has had this solved for years. Laravel, Symfony, and every modern framework ships with the right defaults. The tooling is free. The documentation is everywhere. The only thing standing between your credentials and the internet is knowing the rule — and following it every single time.
You know the rule now.
Your 7-Day Mini-Plan
- Day 1: Audit your current repos. Search commit history for strings like
password,secret,api_key,DB_PASS,token. - Day 2: Set up
.env+phpdotenvin any project that doesn't have it. - Day 3: Rotate any credentials you find in commit history. No exceptions.
- Day 4: Enable GitHub secret scanning on all your repos. Turn on push protection.
- Day 5: Install
truffleHogordetect-secretsand add a pre-commit hook to catch leaks automatically. - Day 6: Document your team's secret management policy — even a README section counts.
- Day 7: Share this with one developer who you suspect has never thought about this. You'll probably be right.
Key metric to track: Number of repos with secrets in commit history. Goal: zero.
Common mistake to avoid: Cleaning up the file but forgetting to rotate the exposed credential. Rotation is always step one. History cleanup is step two.
CTA
Have you ever found an old commit with credentials in it? How did you handle it? Drop it in the comments — no judgment, just lessons. The more honest stories we share, the harder it gets for this to keep catching people off guard. And if this saved you from a future headache, hit the share button. Someone on your team needs to read this today.
Closing Loop
The gap between "I know I shouldn't" and "I won't" is where most breaches live.
It's not ignorance that gets developers into trouble. It's the small compromises — the quick commits, the "just this once," the "it's private anyway." Security isn't a feature you bolt on later. It's a habit you build on day one.
Close the gap. Start today.
"People Also Ask" — 7 Questions & Answers
1. What PHP files should never be committed to Git? Any file containing credentials, API keys, or environment-specific configuration: config.php, db.php, wp-config.php, .env, and database dump files. If it has a password in it, it doesn't belong in your repo.
2. How do I remove a file from Git history after committing it? Use git filter-repo (recommended) or BFG Repo-Cleaner. After rewriting history, force-push with --force-with-lease and immediately rotate any exposed credentials. Don't just delete the file — that only removes it from the latest commit, not the history.
3. Does .gitignore remove a file that's already been committed? No. .gitignore only prevents future tracking. To stop tracking a file that's already committed, run git rm --cached filename and commit that change. Then add the file to .gitignore.
4. Is it safe to commit credentials to a private GitHub repository? It's safer than a public repo, but it's not safe. Private repos can be accidentally made public, cloned by collaborators who later leave, or exposed in a breach. Credentials should never be in any repository.
5. What is the .env file pattern in PHP? A .env file stores environment-specific variables (like database credentials) outside of your codebase. Libraries like vlucas/phpdotenv load these variables into $_ENV at runtime. The .env file is excluded from version control via .gitignore.
6. How quickly can exposed credentials be found after being pushed to GitHub? Security research (GitGuardian, 2023) found that automated bots can detect and exploit newly exposed secrets within minutes of a push. Never assume an exposed credential is safe just because you quickly deleted it.
7. What should I do immediately after accidentally pushing credentials to GitHub? Rotate the credentials first — change passwords, revoke API keys, generate new tokens. Then clean the Git history. In that order. Assume the secret is already compromised from the moment it was pushed.
Note: The $50,000 AWS incident in the hook is a composite illustration of a well-documented class of incidents (AWS credential theft via exposed GitHub repos). For publication, replace with a cited specific incident or clearly label as illustrative. GitGuardian's annual reports contain verified real-world examples that can substitute directly.