How Exposed Secrets Sneak Into Your Repo (and How Audits Find Them)
Nobody commits a secret on purpose
Every leaked credential we’ve ever found in an audit had the same origin story: a reasonable engineer, in a hurry, doing something that felt harmless at the time. Nobody opens a pull request titled “add production database password to repo.” And yet secrets in git are among the most common (and most immediately exploitable) findings in a code audit.
They’re exploitable because the attack is fully automated. Bots scan every public GitHub push within seconds of it landing; leaked AWS keys are routinely used within minutes. Private repos buy you time, not safety: every contractor, ex-employee, CI system, and laptop with a clone has a copy of your history, and repos have a way of becoming public later, through acquisition due diligence, open-sourcing a component, or a fork pushed to the wrong remote.
Here’s how secrets actually get in, and how an audit finds them.
The five ways secrets end up in git
1. The committed .env file
The classic. The project has a .env file, the .gitignore entry was added in commit 47, and the .env file was committed in commit 12. Removing it later doesn’t remove it from history. Variants: .env.local, .env.production, config/secrets.yml, terraform.tfvars.
2. The hardcoded fallback
const stripeKey = process.env.STRIPE_KEY || 'sk_live_51Hx...';Written during a debugging session at 11pm to “just get it working,” then committed along with the actual fix. The fallback pattern is insidious because the code looks like it does the right thing (the environment variable is right there) and the literal after || slides past review.
3. Test fixtures and example configs
config.example.json that someone filled with real values instead of placeholders. Integration tests pointed at a real third-party sandbox using a real key. Seed scripts with a real admin password because “it’s just staging,” and staging shares credentials with production.
4. Documentation and comments
The README with a “quick start” section containing a working database URL. The code comment that says // temp token for testing: ghp_.... Jupyter notebooks are a special hazard: they save cell output, so a printed environment variable gets committed even when the code never contains it.
5. Git history itself
The one everyone forgets. The secret was committed in 2023, someone noticed, deleted the file, committed the deletion, and considered it handled. But git log --all still has it. So does every fork, every clone, and every CI cache. A deleted secret is a published secret. The only real remediation is rotation; the history cleanup (via git filter-repo or BFG) is secondary hygiene, useful mainly so the next scan comes back clean.
How an audit hunts for them
A manual audit layers three techniques, because each catches things the others miss.
Pattern scanning across full history. Tools like gitleaks and trufflehog scan every commit ever made, not just the current tree, for known key formats (AKIA... for AWS, sk_live_ for Stripe, ghp_ for GitHub, xoxb- for Slack) and for high-entropy strings that look like credentials. This is the automated pass, and it’s table stakes.
Reading the config surface by hand. Scanners have blind spots: internal service passwords with no recognizable format, base64-encoded values, secrets split across two variables and concatenated. So the auditor reads everything config-shaped: env handling, deploy scripts, CI YAML, Dockerfiles (a COPY .env . or a build ARG with a default value bakes secrets into image layers), and infrastructure-as-code.
Verifying the live ones. A finding of “string that looks like a key” is noise; “live production Stripe key, currently valid” is a fire alarm. Where it’s safe to do so (a read-only identity call, never anything mutating), an audit distinguishes active credentials from dead ones, because that changes the response from “clean up eventually” to “rotate today.” This triage is a big part of why manual audits find what scanners miss.
Found one? Do these in order
- Rotate first. Revoke the credential and issue a new one. Do this before any git surgery; the git surgery doesn’t un-leak anything.
- Check for use. Audit logs on the affected service: any access you can’t explain, from IPs you don’t recognize?
- Scrub history if warranted with
git filter-repo, coordinating with everyone who has a clone, and remembering forks and CI caches exist. - Fix the intake path so the same secret doesn’t come back next sprint; see below.
Keeping them out
Prevention is cheap and mostly friction-free:
- Pre-commit scanning. gitleaks as a pre-commit hook catches the secret before it becomes history. Seconds per commit; the highest-ROI control on this list.
- CI scanning as backstop. The same scan in CI, run against the diff on every PR, catches whoever skipped the hook.
- A secrets manager, not env-file sharing. AWS Secrets Manager, Vault, Doppler, or even your platform’s built-in config (Fly, Vercel, Railway): anything beats Slacking
.envfiles around, which is how secrets end up in laptops, message history, and eventually commits. - No fallback literals, ever. Fail loudly at startup if a required variable is missing. A crash on boot is a feature; a silent fallback to a live key is an incident.
- Short-lived credentials where the platform supports them. OIDC-based cloud auth for CI instead of long-lived keys removes the most dangerous category entirely.
Most of these are an afternoon of setup. In our experience, the teams that leak secrets aren’t careless; they’re just teams where nobody was ever assigned that afternoon. It’s the same pattern behind most technical debt: not bad engineers, just unowned work.
Get a free code audit
Secret scanning across your full git history (plus the manual config review that catches what pattern-matching misses) is part of every audit we run, and the audit is genuinely free. A Webisoft engineer reviews your repository by hand and sends you a written findings report: what leaked, whether it’s live, what to rotate, and how to keep it from recurring. If you’ve never scanned your history, there’s a real chance something is in there from 2023. Get a free code audit and find out before someone else does.