Topic: static secret detection

MCP server static secret detection — finding hardcoded API keys, tokens, and credentials before they ship

A hardcoded API key in an open-source MCP server is not a private credential — it is a public credential, waiting for the next GitHub search or npm package scrape to surface it. The SkillAudit corpus shows that credential hardcoding is the single most common High-severity finding across community MCP servers. Five detection layers — each catching a different class of secret — eliminate it before code reaches npm or GitHub public.

1. High-entropy regex scanning

The most reliable way to find hardcoded credentials is to scan for strings that have the shape of secrets. API keys, tokens, and credentials share two properties: they match known format patterns, and they have high entropy (many bits of randomness packed into a short string). The combination of format matching and entropy scoring produces very low false-positive rates.

SkillAudit's secret scanner runs two complementary passes:

// All three of these trigger the SkillAudit secret scanner:

// 1. GitHub token format match
const token = 'ghp_A3fKzLm9Nq2Wp5Xr7Yt1Vu8Bw4Es6Cd'  // HIGH: credential pattern

// 2. High-entropy string (no known format, but entropy > 4.5)
const apiKey = 'xK3mP9nQ2rS5tU8vW1yZ4aB7cD0eF6gH'   // HIGH: high-entropy literal

// 3. Fallback in env access — the most common pattern
const key = process.env.API_KEY || 'sk-prod-fallback-key-123'  // HIGH: hardcoded fallback

2. AST-based credential pattern matching

Regex scanning on raw text misses credentials that are constructed dynamically or stored in object literals. AST analysis traverses the code's syntax tree and catches patterns that raw text search misses.

The most important AST patterns for MCP server secret detection:

// Pattern 1: Object literal with credential-looking key names
const config = {
  apiKey: 'sk-prod-abc123',           // AST: string literal under 'apiKey' key
  authorization: 'Bearer eyJ...',     // AST: string literal under 'authorization' key
  password: 'hunter2',                // AST: string literal under 'password' key
}

// Pattern 2: String concatenation building a credential
const auth = 'ghp_' + hardcodedSuffix  // AST: concatenation with known prefix

// Pattern 3: Ternary fallback
const token = process.env.TOKEN ?? 'fallback-token-value'  // AST: nullish coalescing with literal

// SAFE patterns that AST can distinguish:
const apiKey = process.env.API_KEY        // no fallback — safe
const token = process.env.TOKEN!         // non-null assertion — safe
throw new Error('API_KEY required')      // error-on-missing — safe

The AST approach lets SkillAudit distinguish between safe environment variable access (no fallback) and the dangerous pattern of providing a hardcoded fallback value. Regex alone cannot reliably make this distinction.

3. Pre-commit hooks with secretlint

The pre-commit layer catches secrets before they enter the repository — which means before they enter git history, before they're pushed, and before they could ever be published. A secret caught here requires no git history rewrite, no credential rotation announcement, and no repository privacy change.

## Install secretlint as a pre-commit hook

npm install --save-dev secretlint @secretlint/secretlint-rule-preset-recommend husky

## package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "secretlint": {
    "rules": [
      { "id": "@secretlint/secretlint-rule-preset-recommend" }
    ]
  }
}

## .husky/pre-commit
#!/usr/bin/env sh
npx secretlint "**/*"

## Test it:
echo 'const key = "ghp_A3fKzLm9Nq2Wp5Xr7Yt1Vu8Bw4Es6Cd"' > test.ts
git add test.ts && git commit -m "test"
# → secretlint ERROR: Found a credential in test.ts
# → commit aborted

One critical limitation: pre-commit hooks can be skipped with git commit --no-verify. They are a developer-local safety net, not a policy enforcement mechanism. CI scanning (layer 5) is required to enforce the constraint regardless of local bypass.

4. Git history auditing with truffleHog

A secret that was committed and then deleted in a later commit is still present in the git history. Anyone who clones the repository and runs git log --all -p | grep 'ghp_' can recover it. This is not a theoretical attack — GitHub credential scanners and public tooling routinely excavate deleted secrets from repository histories.

If you're onboarding a repository that has had credentials before, history auditing is a mandatory first step:

## Run truffleHog against the full git history
npx trufflehog git file://. --only-verified

## If secrets are found in history, they must be invalidated immediately
## (rotation, not just deletion — they were already exposed)
## Then rewrite history with git-filter-repo:
pip install git-filter-repo
git filter-repo --path-regex ".*" --replace-text <(echo "ghp_A3fKzLm9Nq2==>REDACTED")

## After rewriting, force-push (coordinate with all collaborators first):
git push origin main --force-with-lease

SkillAudit runs a shallow history scan (last 50 commits) as part of every audit. If a credential pattern is found in a commit that precedes a deletion commit, it generates a CRITICAL finding regardless of whether the current HEAD is clean — because the credential is still recoverable from history and should be rotated.

5. Trivy secret scanning in CI

Trivy is a general-purpose security scanner that includes a high-quality secret detection engine. Running it in CI provides the enforcement layer that pre-commit hooks cannot: it runs on every push, it cannot be bypassed with --no-verify, and it can block the build before the code reaches a release pipeline.

## .github/workflows/secret-scan.yml
name: Secret Detection

on: [push, pull_request]

jobs:
  trivy-secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for git-log scanning

      - name: Run Trivy secret scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          scanners: 'secret'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'  # Fail the build on findings

      - name: Scan git history
        run: |
          docker run --rm -v "$(pwd):/repo" \
            trufflesecurity/trufflehog:latest \
            git file:///repo --only-verified --fail

Configure Trivy to fail on HIGH and CRITICAL severity findings. MEDIUM findings (such as generic high-entropy strings that may not be real credentials) can be reviewed manually without blocking the build — a false-positive triage step that keeps the CI gate from generating noise.

What SkillAudit checks for static secrets

SkillAudit's Credential Exposure scan combines all five layers. The findings are reported in the Security sub-score with the following severity mapping:

A CRITICAL or HIGH secret finding on the Credential Exposure sub-score is a sub-score blocker: the Credential Exposure sub-score is capped at 30/100 regardless of other credential practices, because a confirmed exposed secret invalidates the entire credential hygiene picture.

See the C-to-A remediation guide for the recommended fix sequence when a scan returns a secret finding. See API key rotation security for the rotation workflow once a leaked secret is discovered.

Scan for hardcoded secrets now

SkillAudit's Credential Exposure scan runs automatically on every audit — no configuration required. Paste your GitHub URL to see the report.

Run a free audit