NEWBravos is availableCreate your account
The Knowledge Base
15 min read

Git History Secret Scanning: Detecting Leaked Credentials in Repositories

Infrastructure SecurityBy BugB Security Team

Scan Git repository history for leaked API keys tokens and credentials.

Executive Summary

Exposed .git directories are one of the most common and damaging misconfigurations in web deployments. When developers deploy applications directly from a git clone without blocking access to the .git/ folder, attackers gain access to the full version control history — including every secret ever committed, even if later deleted.

The result? Complete credential compromise. An attacker can reconstruct database passwords, API keys, cloud access tokens, private keys, and deploy credentials that were committed months or years ago and "cleaned up" in a subsequent commit.

Key Insight: Deleting a secret in a new commit does NOT remove it from git history. The original blob objects remain fully accessible and reconstructible from the .git/objects/ directory. CERT-X-GEN detects both the directory exposure and the direct credential leakage in readable git metadata files.

Quick Stats

MetricValue
CVSS Score9.8 (Critical)
CWECWE-312 (Cleartext Storage of Sensitive Information)
Affected SystemsAny web server without /.git/ access controls
Detection ComplexityLow–Medium (requires HTTP + regex scanning)
Exploitation DifficultyTrivial (curl + git commands)
FOFA Coverage219+ indexed exposed instances

Run this check with CERT-X-GEN — the polyglot security scanner that executes templates in real programming languages. Get the templates | Install CERT-X-GEN

Template source: git-history-secret-scan.go

Understanding the Vulnerability

How It Happens

Modern development teams use git for version control. The typical vulnerable deployment pattern is:

Developer machine Production server
┌─────────────────┐ ┌──────────────────────────────┐
│ git clone │ │ /var/www/html/ │
│ myapp/ │ scp / rsync │ index.php │
│ ├── .git/ ───────────────────►│ app.js ← exposed! │
│ ├── index.php │ │ .git/ ← DANGER! │
│ └── app.js │ │ HEAD │
└─────────────────┘ │ config ← has creds │
 │ logs/HEAD ← has SHAs │
 │ objects/ ← has blobs │
 └──────────────────────────────┘

The developer copies the app files to the server — including the hidden .git/ directory — and the web server happily serves it without any access restrictions.

The Three Layers of Exposure

LayerFilesWhat's ExposedSeverity
L1 — Directory Accessible.git/HEADBranch name, confirms git repo exposureMedium
L2 — Config Leak.git/configRemote URLs (may contain embedded credentials), repository metadataHigh–Critical
L3 — Secret in HistoryCOMMIT_EDITMSG, logs/HEAD, logs/refs/heads/*API keys, passwords, tokens in commit messages and log entriesCritical

Why "Deleting" Secrets Doesn't Help

Commit A: "add DB config" ← DB_PASSWORD=Passw0rd123! is stored in blob object
 ↓
Commit B: "cleanup secrets" ← .env removed from tree, but blob object REMAINS
 ↓
 git object store still has the blob
 curl /.git/objects/ab/cdef... → returns the original file content

The git object model is append-only by design. Without running git filter-repo or BFG Repo Cleaner AND force-pushing, the secrets remain permanently accessible.


Why Traditional Scanners Fail

The YAML Limitation

Traditional YAML-based scanners like Nuclei can detect the presence of a .git directory but cannot correlate findings across multiple files or extract and classify secrets:

yaml
# What a YAML scanner CAN do:
id: git-directory-exposure
requests:
 - method: GET
 path:
 - "{{BaseURL}}/.git/HEAD"
 matchers:
 - type: word
 words:
 - "ref: refs/heads/"

This produces a single low-confidence "exposed git directory" finding with no context about actual impact.

CapabilityYAMLCERT-X-GEN
Detect .git/HEAD accessible
Fetch and parse .git/config
Extract remote URL with embedded creds
Scan logs/HEAD for secret patterns
Match 13+ secret pattern types
Redact matched secrets in output
Severity escalation based on findings
Detect creds-in-URL vs creds-in-files
Confidence Level~30%95%

The Detection Gap

A YAML scanner tells you "the .git directory is accessible." CERT-X-GEN tells you "the .git directory is accessible, the remote URL contains a GitHub deploy token, and a Stripe live key was found in 3 git log files."


The CERT-X-GEN Approach

The template uses Go's standard library to perform a systematic multi-layer inspection of all accessible git metadata files, classify findings by severity, and redact sensitive values to avoid storing live credentials in scan reports.

Detection Strategy

┌──────────────────────────────────────────────────────────────────┐
│ CERT-X-GEN DETECTION FLOW │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Scanner ──► GET /.git/HEAD │
│ │ ├── 200 + "ref: refs/heads/" → LAYER 1 CONFIRMED │
│ │ └── 404 / redirect / non-git body → STOP │
│ ▼ │
│ Scanner ──► GET /.git/config │
│ │ ├── Parse [remote "origin"] url │
│ │ ├── Regex: credentials in URL? │
│ │ │ └── YES → CRITICAL: Creds in Remote URL │
│ │ └── Store config evidence │
│ ▼ │
│ Scanner ──► GET /.git/COMMIT_EDITMSG │
│ GET /.git/logs/HEAD │
│ GET /.git/logs/refs/heads/main │
│ GET /.git/logs/refs/heads/master │
│ GET /.git/refs/heads/main │
│ GET /.git/refs/heads/master │
│ GET /.git/config (re-scan for secrets) │
│ │ ├── Run 13 secret patterns against each file │
│ │ ├── Deduplicate across files │
│ │ ├── Redact matched values (show first/last 4 chars) │
│ │ └── Match found? → CRITICAL: Secrets in History │
│ ▼ │
│ Output: JSON findings array, severity-escalated, redacted │
│ │
└──────────────────────────────────────────────────────────────────┘

Key Advantages

  1. Multi-layer correlated detection: Single pass covers 3 distinct exposure vectors
  2. Severity escalation: Base finding starts at MEDIUM, upgrades to CRITICAL when evidence warrants
  3. Credential redaction: Matched secrets are masked (sk_l****...****0C) — proof without risk
  4. 13 secret pattern types: AWS, GitHub, Stripe, Google, Slack, Twilio, generic API keys, passwords, DB URLs, private keys, bearer tokens, credentials in URLs
  5. Port-aware scanning: Correctly handles non-standard ports (:8765, :3000, :8888) via CERT_X_GEN_TARGET_PORT
  6. TLS-flexible: Disables certificate verification for self-signed staging/dev deployments
  7. Redirect rejection: Does not follow redirects on .git/HEAD — a redirect means the path is protected

Attack Flow Visualization

Complete Attack Chain (Real-World Attacker Perspective)

Phase 1: Discovery

bash
# Attacker discovers exposed .git directory
curl https://victim.com/.git/HEAD
# → ref: refs/heads/main

Phase 2: Reconnaissance

bash
# Extract remote URL (may contain deploy credentials)
curl https://victim.com/.git/config
# → url = https://deploy:ghp_TOKEN@github.com/company/app.git

# Read recent commit history
curl https://victim.com/.git/logs/HEAD
# → commit SHAs + messages (may contain secrets in commit messages)

Phase 3: History Reconstruction

bash
# Download pack files to reconstruct full history
curl https://victim.com/.git/packed-refs
curl https://victim.com/.git/objects/pack/pack-<hash>.idx
curl https://victim.com/.git/objects/pack/pack-<hash>.pack

# Extract blobs from object store using commit SHAs
git init recovered && cd recovered
git fetch https://victim.com/.git
git checkout FETCH_HEAD
# → Full source code + all historical .env files

Phase 4: Credential Extraction

bash
# Enumerate all commits and search for secrets
git log --all --full-history -- "*.env" "*.key" "*.conf"
git show <commit-sha>:.env
# → DB_PASSWORD=Passw0rd123!
# → AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI...

Severity Decision Tree

GET /.git/HEAD
 │
 ├── NOT 200 or not git content ──────────────────► No findings (exit)
 │
 └── 200 + "ref: refs/heads/..." ──────────────────► MEDIUM: .git Exposed
 │
 ├── .git/config accessible?
 │ └── remote URL has credentials? ──► CRITICAL: Creds in Config
 │
 └── Any secret pattern matched in log/commit files?
 └── YES ──────────────────────────► CRITICAL: Secrets in History

Template Deep Dive

Secret Pattern Engine

The template implements 13 secret detection patterns covering the most common credential types found in repositories:

go
var secretPatterns = []SecretPattern{
 {Name: "AWS Access Key ID",
 Pattern: regexp.MustCompile(`(?i)AKIA[0-9A-Z]{16}`), Redact: true},

 {Name: "GitHub Token",
 Pattern: regexp.MustCompile(`gh[pousr]_[A-Za-z0-9_]{36,255}`), Redact: true},

 {Name: "Stripe Key",
 Pattern: regexp.MustCompile(`(?:r|s)k_(live|test)_[A-Za-z0-9]{24,}`), Redact: true},

 {Name: "Database URL with Credentials",
 Pattern: regexp.MustCompile(`(?i)(mysql|postgres|mongodb|redis|amqp|ftp)://[^:]+:[^@\s]+@`),
 Redact: true},

 {Name: "Private Key Header",
 Pattern: regexp.MustCompile(`-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----`),
 Redact: false}, // Key header itself is not sensitive
 // ... 8 more patterns
}

Credential Redaction

Matched secrets are never stored in plain text in scan results:

go
func scanForSecrets(content string) []secretMatch {
 // ...
 if sp.Redact && len(preview) > 12 {
 // Show first 4 + last 4 chars with asterisks in between
 // sk_live_51Hzwrt... → sk_l****...****lo2C
 preview = preview[:4] + strings.Repeat("*", len(preview)-8) + preview[len(preview)-4:]
 }
}

This means scan reports are safe to share with stakeholders — they prove the secret exists without reproducing the live credential.

Port-Aware URL Construction

A key engineering decision: the template respects CERT_X_GEN_TARGET_PORT and handles non-standard ports correctly.

go
func normalizeTarget(host string, port int) []string {
 // Strip any existing scheme from host
 // Handle host:port notation in the host string itself
 switch {
 case port == 443:
 return []string{"https://" + host}
 case port == 80 || port == 0:
 return []string{"http://" + host, "https://" + host}
 default:
 // Non-standard port: try http first, then https
 return []string{
 fmt.Sprintf("http://%s:%d", host, port),
 fmt.Sprintf("https://%s:%d", host, port),
 }
 }
}

This is critical because many development and staging servers with exposed .git directories run on ports like 3000, 7777, or 8888.


Usage Guide

Basic Usage

bash
# Scan a single target
cxg scan --scope example.com --template templates/git-history-secret-scan/git-history-secret-scan.go

# With explicit port (critical for non-standard ports)
cxg scan --scope example.com:3000 --template templates/git-history-secret-scan/git-history-secret-scan.go

# JSON output with verbose logging
cxg scan --scope @targets.txt --template templates/git-history-secret-scan/git-history-secret-scan.go --output-format json --timeout 30s -vv

# HTTPS target
cxg scan --scope secure.example.com:443 --template templates/git-history-secret-scan/git-history-secret-scan.go

Targets File Format

text
# git-history-targets.txt
example.com:80
staging.company.com:3000
192.168.1.100:8080
dev.internal.app:8765

Direct Template Execution

bash
# Build and run directly
cd templates/git-history-secret-scan
go build -o git-history-secret-scan git-history-secret-scan.go

# Run with env vars (engine mode)
CERT_X_GEN_TARGET_HOST=example.com CERT_X_GEN_TARGET_PORT=80 CERT_X_GEN_MODE=engine ./git-history-secret-scan

# Run with CLI flags
./git-history-secret-scan --target example.com --port 3000

Local Docker Test Environment

bash
# 1. Create a test git repo with simulated secrets
mkdir -p /tmp/git-test && cd /tmp/git-test
git init testrepo && cd testrepo
git config user.email 'test@test.com' && git config user.name 'Test'
git remote add origin https://deploy:ghp_FAKTOKEN@github.com/example/app.git
echo 'readme' > readme.txt && git add . && git commit -m 'sk_live_FAKESTRIPEKEY added'

# 2. Start nginx serving the repo without .git protection
docker run -d --name git-test -p 8765:8765 \
 -v $(pwd):/usr/share/nginx/html:ro \
 -v /path/to/nginx.conf:/etc/nginx/conf.d/default.conf \
 nginx:alpine

# 3. Verify and scan
curl http://localhost:8765/.git/HEAD
cxg scan --scope localhost:8765 \
 --template templates/git-history-secret-scan/git-history-secret-scan.go \
 --output-format json --timeout 30s -vv

# 4. Cleanup
docker rm -f git-test

Expected Output — CRITICAL Finding (Secrets Detected)

json
[
 {
 "template_id": "git-history-secret-scan",
 "severity": "critical",
 "confidence": 95,
 "title": "Secrets Found in Exposed Git History (4 pattern(s) matched)",
 "description": "The exposed .git directory on http://localhost:8765 contains files with secret credential patterns. 4 secret(s) matched across 4 git file(s): COMMIT_EDITMSG, logs/HEAD, logs/refs/heads/master, config.",
 "evidence": {
 "secret_matches": [
 {"pattern_name": "Stripe Key", "preview": "sk_l****...****lo2C"},
 {"pattern_name": "GitHub Token", "preview": "ghp_****...****FAKE"},
 {"pattern_name": "Credentials in URL", "preview": "http****...****git"}
 ],
 "affected_git_files": ["COMMIT_EDITMSG", "logs/HEAD", "logs/refs/heads/master", "config"],
 "total_secrets": 4
 },
 "cwe": "CWE-312",
 "cvss_score": 9.8
 },
 {
 "template_id": "git-history-secret-scan",
 "severity": "critical",
 "confidence": 90,
 "title": "Git Config Exposes Credentials in Remote URL",
 "description": "The .git/config file contains a remote URL with embedded credentials.",
 "evidence": {
 "git_config": {
 "config_url": "http://localhost:8765/.git/config",
 "remote_origin_url": "https://deploy:ghp_abc123@github.com/example/app.git",
 "credentials_in_remote_url": true
 }
 },
 "cwe": "CWE-312",
 "cvss_score": 9.8
 }
]

Expected Output — Clean Target (No Exposure)

json
[]

An empty array with exit code 0 means no .git directory was found accessible. The engine logs [-] /.git/HEAD not accessible (status=404) to stderr.


Real-World Test Results

The template was tested against live FOFA-discovered targets and a local Docker environment:

FOFA Scan (5 Targets — body="ref: refs/heads/")

TargetPortCountry.git/HEADSecrets FoundNotes
nips.nostr.com80DE404N/APatched since FOFA crawl
161.35.13.1773000US404N/AHTML error page
8.155.171.678888CN200 (non-git)N/AChinese app, blocked path
144.91.115.224443DEEmptyN/AConnection refused
45.33.68.2680US404N/APatched since FOFA crawl

Key Finding: All 5 FOFA targets had patched or removed the .git exposure since FOFA's last crawl. The template correctly returned [] with zero false positives and graceful handling for all unreachable or protected targets.

Docker Local Test (Confirmed Vulnerable Instance)

TargetPort.git/HEAD.git/configSecrets FoundSeverity
localhost8765ref: refs/heads/masterRemote URL with credentials4 patterns × 4 filesCRITICAL

Detected patterns:

  • Stripe Key (sk_live_*) in COMMIT_EDITMSG
  • GitHub Token (ghp_*) in COMMIT_EDITMSG, logs/HEAD, logs/refs/heads/master
  • Credentials in URL in config

Scan duration: 0.45 seconds for 1 target — extremely fast due to Go's efficient HTTP client.


Defense & Remediation

Immediate Actions (Do These Now)

1. Block .git/ at the web server

nginx
# nginx — add to server block
location ~ /\.git {
 deny all;
 return 404;
}
apache
# Apache — add to .htaccess or VirtualHost
RedirectMatch 404 /\.git
# OR
<DirectoryMatch "^/.*/\.git/">
 Order deny,allow
 Deny from all
</DirectoryMatch>

2. Rotate ALL secrets found in history

Do not assume a deleted secret is safe. Rotate every credential that ever appeared in any commit:

  • Database passwords → change immediately
  • API keys → revoke and regenerate
  • Cloud access keys (AWS, GCP, Azure) → deactivate and rotate
  • Deploy tokens → revoke from VCS provider

3. Purge secrets from git history

bash
# Using git-filter-repo (recommended over BFG for modern repos)
pip install git-filter-repo
git filter-repo --path .env --invert-paths
git filter-repo --replace-text <(echo 'AKIAIOSFODNN7EXAMPLE==>***REDACTED***')

# Force push to all remotes
git push --force --all
git push --force --tags

# All clones must be re-cloned (old clones retain the history)

Deployment Best Practices

Never deploy directly from a git clone. Use a CI/CD pipeline that copies only application files:

yaml
# GitHub Actions — secure deployment
- name: Deploy application files only
 run: |
 rsync -av --exclude='.git' --exclude='.env' \
 ./app/ user@server:/var/www/html/

Use .gitignore aggressively:

gitignore
# Never commit these
.env
.env.*
*.key
*.pem
config/secrets.yml
credentials.json
aws_credentials

Pre-commit secret scanning:

bash
# Install Gitleaks pre-commit hook
gitleaks protect --staged --config .gitleaks.toml

Detection Checklist

Configuration:

  • Block /.git/ at web server level (nginx/Apache/Caddy)
  • Use CI/CD pipelines — never scp or rsync with .git/ included
  • Store secrets in vault (HashiCorp Vault, AWS Secrets Manager, Doppler)
  • Use environment variables — never hardcode in source files

Remediation:

  • Rotate all credentials found in any historical commit
  • Use git filter-repo to purge secrets from history
  • Notify all stakeholders with access to old clones

Monitoring:

  • Run CERT-X-GEN git-history-secret-scan on all web assets regularly
  • Set up Gitleaks in CI/CD pre-commit and pre-push hooks
  • Enable GitHub/GitLab secret scanning on all repositories

Framework-Specific Server Configurations

ServerSecure Configuration
nginxlocation ~ /\.git { deny all; return 404; }
ApacheRedirectMatch 404 /\.git
Caddyrespond /\.git/* "Not Found" 404
IISRequest filtering → deny .git path
TraefikMiddleware with stripPrefix + 404 response

Extending the Template

Adding New Secret Patterns

go
// Add to secretPatterns slice in the template
{
 Name: "Anthropic API Key",
 Pattern: regexp.MustCompile(`sk-ant-[A-Za-z0-9\-_]{40,}`),
 Redact: true,
},
{
 Name: "OpenAI API Key",
 Pattern: regexp.MustCompile(`sk-[A-Za-z0-9]{48}`),
 Redact: true,
},
{
 Name: "Cloudflare API Token",
 Pattern: regexp.MustCompile(`[A-Za-z0-9_-]{40}cloudflare`),
 Redact: true,
},

Scanning Additional Git Files

go
// Extend secretScanFiles to include more git metadata
secretScanFiles := []string{
 "COMMIT_EDITMSG",
 "logs/HEAD",
 "logs/refs/heads/main",
 "logs/refs/heads/master",
 "logs/refs/heads/develop", // Add common branch names
 "refs/heads/main",
 "refs/heads/master",
 "config",
 "description", // Sometimes contains repo info
 "info/exclude", // May reveal what was intentionally excluded
}

Pack File Object Reconstruction (Advanced)

For deep history scanning, extend the template to download and parse git pack files:

go
// Fetch pack index to enumerate all object SHAs
func fetchPackIndex(ctx context.Context, client *http.Client, baseURL string) []string {
 body, status, _ := fetchGitFile(ctx, client, baseURL, "objects/info/packs")
 if status != 200 { return nil }
 // Parse pack filenames from response
 // Download .idx files to enumerate blob SHAs
 // Download and decompress .pack files for blob content
 // ... (advanced reconstruction logic)
}

Integration with CI/CD

yaml
# GitHub Actions — scan on every deployment
name: Git Exposure Scan
on:
 push:
 branches: [main, staging]

jobs:
 git-exposure-check:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v3
 - name: Run Git History Secret Scan
 run: |
 cxg scan \
 --scope ${{ secrets.STAGING_URL }} \
 --template templates/git-history-secret-scan/git-history-secret-scan.go \
 --output-format json \
 --timeout 30s \
 --output git-exposure-results.json
 - name: Check for critical findings
 run: |
 CRITICAL=$(jq '.findings | map(select(.severity == "critical")) | length' git-exposure-results.json)
 if [ "$CRITICAL" -gt "0" ]; then
 echo "CRITICAL: .git directory exposed with $CRITICAL finding(s)"
 exit 1
 fi

References

CVE Database & Advisories

ReferenceDescription
CWE-312Cleartext Storage of Sensitive Information
CWE-538Insertion of Sensitive Information into Externally-Accessible File or Directory
OWASP A05:2021Security Misconfiguration
OWASP WSTG-CONF-05Enumerate Infrastructure and Application Admin Interfaces

Tools & Resources

Research & Write-ups

  1. Mao, S. (2014). "Hacking websites with git" — Original research on .git directory exposure
  2. HackerOne Reports — Numerous critical bounties for exposed .git directories on production servers
  3. SANS Institute — "The Dangers of Exposed .git Directories in Production"

Need help hardening your environment?

Talk to the Bugb security team directly.

Book a session