DEV Community

Cenk KURTOĞLU
Cenk KURTOĞLU

Posted on

MCP configs are the new .env files — I found live database keys committed on GitHub

MCP configs are the new .env files. I scanned GitHub and found live database keys committed in public repos.

If you use Claude Desktop, Claude Code, Cursor or Windsurf with external tools, you have an .mcp.json or claude_desktop_config.json file on your machine. It tells your editor which MCP servers to start, and each server entry can carry an env block.

That env block is a secret store. And developers are pasting real keys into it — then committing the file.

What I found

I ran GitHub code search for filename:.mcp.json SUPABASE_SERVICE_ROLE and similar queries, then verified every result instead of trusting the match:

  1. Decode, don't pattern-match. A file only counts if it contains a JWT whose decoded payload literally says "role": "service_role". In my scan, 53 of 65 candidate files were placeholders, templates or examples. If I had emailed all 65, four out of five would have been noise — the fastest way to teach someone to ignore security reports.
  2. Probe whether the project is live. A leaked key for a deleted Supabase project is worthless. A GET https://<ref>.supabase.co/rest/v1/ that answers 401 (no credentials sent, nothing read) proves the project is live and the key is valid until rotated.

The result: 12 files in public repos contain a real, decodable service_role key. 4 of those projects are live right now. One of them was pushed the same day I scanned.

A service_role key bypasses every row-level-security policy. Whoever finds it has full read/write access to the database — not "some" access, all of it.

Why this keeps happening

The mental model is wrong, not the people. .mcp.json feels like editor configuration — like settings.json, like .vscode/. Nobody expects a linter or a framework to warn them, because no tool in this ecosystem treats the file as a secret store yet.

Compare with .env: a decade of leaked keys produced pre-commit hooks, secret scanners, CI checks, and platform warnings. .mcp.json has none of that guardrail history, so the same 2015-style mistakes are being made again in 2026 — just with database keys that grant more access than most API keys ever did.

Deleting the file doesn't help either: git history keeps every version forever. The only fix that works is rotating the key (Supabase Dashboard → Settings → API → rotate), then moving the new value into a runtime environment variable.

I built the linter I wanted to exist

I've spent months doing Supabase RLS audits. The configuration side of this problem needs zero-expertise tooling, so I built a family of zero-dependency auditors. Each one runs locally, reads nothing from the network, and redacts evidence in its reports so the report itself is safe to paste into an issue:

  • mcp-config-security — npx mcp-config-security. Scans Claude Desktop, Claude Code, Cursor and Windsurf configs for service_role JWTs (decoded, so a token hiding under a neutral key name is still caught), real-looking tokens in env/headers, --dangerously-skip-permissions flags, silent autoApprove on write tools, plaintext http:// remotes, disabled TLS verification, and filesystem servers rooted at /.
  • supabase-service-role-scanner — npx supabase-service-role-scanner. Walks a project for NEXT_PUBLIC_*SERVICE_ROLE* variables, NEXT_PUBLIC_ values carrying non-anon JWTs, and service_role literals in source. The anon key in NEXT_PUBLIC_ is correct by design and never flagged.
  • github-actions-audit — npx github-actions-audit. Static scan of workflow YAML for pull_request_target pwn requests, ${{ github.event.pull_request.title }}-style shell injection, hardcoded secrets, and unpinned actions.
  • firebase-rules-audit — npx firebase-rules-audit. Finds allow read, write: if true;, request.auth == null grants and conditions with no auth check at all, in Firestore and Storage rules.
  • n8n-workflow-audit — npx n8n-workflow-audit. Exported workflow JSON often carries tokens pasted straight into HTTP Request nodes. This flags hardcoded credentials, ?apikey= in URLs and unauthenticated webhooks — while treating n8n's own credentials: { id, name } references as the correct pattern.

All free, all MIT, no accounts, no telemetry. The companion repo supabase-rls-leak-demo covers the database side: a fixture that goes red on a broken RLS policy and green on the one-file fix.

My own tool flagged my own CIs

The first thing I ran against my own four repos was github-actions-audit. It came back with 20 findings: every actions/checkout@v4 and actions/setup-node@v4 was a mutable tag, and none of the workflows had a permissions: block.

That is the correct experience for this class of tool — it should be slightly embarrassing. I resolved the action tags to their commit SHAs (git.ref/tags/v4), pinned all four workflows, added permissions: contents: read, and pushed. Same command now exits 0. If a security linter finds nothing on its first run against your config soup, be suspicious of the linter.

What to do tonight

If you have an .mcp.json or claude_desktop_config.json anywhere in a repo — or a NEXT_PUBLIC_ Supabase variable you're not sure about:

npx mcp-config-security
npx supabase-service-role-scanner .
Enter fullscreen mode Exit fullscreen mode

Two commands, a few seconds, zero installs. If they come back clean, you're done. If they don't, rotate first, refactor second.

I reported the live leaks I found privately — no bounties asked, just the rotation. If one of these tools catches something real for you, you can buy me a coffee, but the tools stay free either way.

Top comments (3)

Collapse
 
cekuu35 profile image
Cenk KURTOĞLU •

Agreed on all of it — treat them exactly like .env files.

Two small additions from the trenches:

  1. gitignore alone isn't enough because the config usually has to exist for the agent to run at all. The pattern that survives is .mcp.json as a redacted template plus a gitignored local override (.mcp.local.json) holding real values — most clients honor a local-override chain, and the "teammate clones the repo at 2am" case, which is where most of these leaks actually originate, gets the template instead of the keys.

  2. For the pre-commit hook, generic secret scanners (gitleaks & co.) catch the common token formats, but Supabase JWTs need actual payload decoding to tell a real service_role key from a placeholder that looks like one — in the batch I scanned, 53 of 65 candidate files were fakes that a pattern-matcher would have flagged. That's the difference between a scanner your team trusts and one they learn to ignore.

Collapse
 
aikotanaka profile image
Aiko Tanaka •

Treat MCP config files like application secret files. Keep real credentials out of version control, commit a redacted example, and inject secrets at runtime from a password manager or secret store. Add config filenames and local overrides to .gitignore, but also run secret scanning in pre-commit and CI because ignore rules do not protect files that are already tracked. If a key reaches GitHub, removing the commit is not enough. Revoke or rotate it immediately, review the provider’s audit logs, and assume any high-privilege token may already have been copied. Least-privilege, short-lived credentials also limit the damage if a config leaks.

Collapse
 
abhinavs1920 profile image
Abhinav Sharma •

The decode-don't-pattern-match step is what separates a report from noise - 53 of 65 being placeholders is a humbling ratio. Every new tool that reads a local config with an env block is a new place secrets can leak from, and gitignore habits haven't caught up.