Secrets Management & Leakage Code Review Guide
Table of Contents
Introduction
Secrets leakage is one of the most common and damaging security vulnerabilities in modern software development. API keys, database credentials, encryption keys, and other sensitive data frequently end up exposed in source code, logs, or public repositories, leading to data breaches and infrastructure compromise.
According to GitGuardian's State of Secrets Sprawl report, over 10 million secrets were detected in public GitHub commits in a single year. Many organizations are unaware their secrets have been exposed until attackers have already exploited them.
Secrets Are Actively Hunted
Attackers run automated scanners on public repositories 24/7, looking for exposed credentials. A leaked AWS key can be exploited within minutes of being pushed to GitHub. The average cost of a secrets-related breach exceeds $4 million.
Why are exposed secrets particularly dangerous compared to other vulnerabilities?
Types of Secrets
Understanding the different types of secrets helps prioritize protection efforts. Each type has unique exposure risks and requires specific handling procedures.
Types of Secrets to Protect
Third-party services
Connection strings
AES, RSA private keys
Client secrets, tokens
TLS, signing certs
AWS, GCP, Azure
Server access
Signature validation
Secret Types and Risk Levels
| Secret Type | Example | Impact if Exposed | Priority |
|---|---|---|---|
| Cloud Provider Keys | AWS Access Key ID | Full infrastructure compromise | Critical |
| Database Credentials | PostgreSQL password | Data breach, data loss | Critical |
| API Keys (paid) | Stripe, Twilio keys | Financial loss, service abuse | High |
| Encryption Keys | AES-256 key, RSA private | Data decryption, impersonation | Critical |
| OAuth Secrets | Client secret | Account takeover | High |
| JWT Signing Keys | HMAC secret | Authentication bypass | Critical |
| SSH Private Keys | id_rsa | Server access | Critical |
| Webhook Secrets | GitHub webhook secret | Event injection | Medium |
Which exposed secret typically has the most severe impact?
Exposure Vectors
Secrets can leak through numerous channels throughout the development lifecycle. Understanding these vectors is essential for comprehensive protection.
Common Secrets Exposure Vectors
- • Hardcoded credentials
- • Config files in repos
- • .env files committed
- • Git history exposure
- • Debug logging secrets
- • Error stack traces
- • Console output
- • Log aggregation services
- • Exposed env variables
- • Container images
- • Build artifacts
- • CI/CD pipelines
Impact: Exposed secrets lead to unauthorized access, data breaches, and infrastructure compromise. Once a secret is exposed, it must be considered compromised and rotated immediately.
Common Exposure Patterns
1# Source Code Exposure
2- Hardcoded credentials in application code
3- Configuration files with secrets
4- .env files committed to repositories
5- Secrets in code comments
6- Base64 "encoded" (not encrypted) secrets
7
8# Git History Exposure
9- Secret committed then "removed" (still in history)
10- Force-pushed sensitive branches
11- Merged feature branches with secrets
12- Git submodules with credentials
13
14# Log Exposure
15- Debug logging of request/response with auth headers
16- Error messages containing connection strings
17- Stack traces with environment variables
18- Audit logs with unmasked sensitive data
19
20# Build & Deployment Exposure
21- Secrets in Docker images (ENV, build args)
22- CI/CD pipeline logs
23- Build artifacts and packages
24- Infrastructure-as-code templates
25
26# Client-Side Exposure
27- Secrets in JavaScript bundles
28- Mobile app embedded credentials
29- Browser localStorage/sessionStorage
30- Network requests in browser dev toolsThe Git History Problem
Simply deleting a file with secrets and committing doesn't remove it from history. The secret remains accessible through git log, git show, and repo cloning. A complete history rewrite (git filter-branch or BFG) is required, and even then, forks and cached copies may retain the secret.
Code Review Patterns
During code review, look for these patterns that indicate potential secrets exposure. Many secrets follow predictable formats that can be detected with regex patterns.
Hardcoded Secrets Patterns
1// VULNERABLE: Hardcoded API keys
2const API_KEY = 'sk_live_51H7xK2GpE...'; // Stripe key pattern
3const AWS_SECRET = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCY...'; // AWS pattern
4
5// VULNERABLE: Database connection strings
6const dbUrl = 'postgresql://admin:SuperSecret123@prod-db.example.com:5432/main';
7const mongoUri = 'mongodb+srv://user:password@cluster.mongodb.net/db';
8
9// VULNERABLE: Inline credentials
10const client = new Client({
11 apiKey: 'api_key_12345', // Should be from env
12 secret: 'secret_abcdef'
13});
14
15// VULNERABLE: Base64 "obfuscation" (not encryption!)
16const password = Buffer.from('U3VwZXJTZWNyZXQxMjM=', 'base64').toString();
17
18// VULNERABLE: Credentials in URLs
19fetch('https://user:pass@api.example.com/data');
20
21// VULNERABLE: Comments with secrets
22// TODO: Remove before production - API key: sk_test_123456
23// Old key: AKIAIOSFODNN7EXAMPLE (keeping for reference)Secret Detection Regex Patterns
1// Common secret patterns to search for in code review
2const secretPatterns = {
3 // AWS
4 awsAccessKey: /AKIA[0-9A-Z]{16}/,
5 awsSecretKey: /[A-Za-z0-9\/+=]{40}/,
6
7 // GitHub
8 githubToken: /ghp_[a-zA-Z0-9]{36}/,
9 githubOAuth: /gho_[a-zA-Z0-9]{36}/,
10
11 // Stripe
12 stripeKey: /sk_live_[a-zA-Z0-9]{24,}/,
13 stripeTestKey: /sk_test_[a-zA-Z0-9]{24,}/,
14
15 // Google
16 googleApiKey: /AIza[0-9A-Za-z\-_]{35}/,
17 googleOAuth: /[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com/,
18
19 // Generic patterns
20 privateKey: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
21 genericSecret: /(api[_-]?key|apikey|secret|password|token|auth)['"]?\s*[:=]\s*['"][^'"]{8,}/i,
22 connectionString: /(mysql|postgresql|mongodb|redis):\/\/[^\s]+/i,
23
24 // JWT
25 jwt: /eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*/,
26
27 // Slack
28 slackToken: /xox[baprs]-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24}/,
29 slackWebhook: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[a-zA-Z0-9]+/
30};Why is Base64 encoding NOT a secure way to store secrets?
Git & Version Control
Git is a common source of secrets leakage. Even after deletion, secrets remain in repository history. Prevention requires both technical controls and developer awareness.
Essential .gitignore Entries
1# Environment files
2.env
3.env.*
4!.env.example
5.envrc
6
7# Secrets and credentials
8*.pem
9*.key
10*.p12
11*.pfx
12credentials.json
13secrets.json
14service-account*.json
15
16# IDE and local configs
17.idea/
18.vscode/settings.json
19*.local
20
21# Build outputs that might contain secrets
22dist/
23build/
24.next/
25node_modules/
26
27# Docker
28docker-compose.override.yml
29
30# Terraform state (contains secrets!)
31*.tfstate
32*.tfstate.*
33.terraform/Pre-commit Hook for Secret Detection
1#!/bin/bash
2# .git/hooks/pre-commit
3# Install: chmod +x .git/hooks/pre-commit
4
5echo "Scanning for secrets..."
6
7# Check for AWS keys
8if git diff --cached --name-only | xargs grep -l "AKIA[0-9A-Z]\{16\}" 2>/dev/null; then
9 echo "ERROR: Possible AWS Access Key detected!"
10 exit 1
11fi
12
13# Check for private keys
14if git diff --cached --name-only | xargs grep -l "BEGIN.*PRIVATE KEY" 2>/dev/null; then
15 echo "ERROR: Private key detected!"
16 exit 1
17fi
18
19# Check for common secret patterns
20if git diff --cached | grep -iE "(password|secret|api_key|apikey)\s*[=:]\s*['"][^'"]{8,}" 2>/dev/null; then
21 echo "ERROR: Possible hardcoded secret detected!"
22 exit 1
23fi
24
25# Use gitleaks if available
26if command -v gitleaks &> /dev/null; then
27 gitleaks detect --staged --verbose
28 if [ $? -ne 0 ]; then
29 echo "ERROR: Gitleaks found potential secrets!"
30 exit 1
31 fi
32fi
33
34echo "No secrets detected."
35exit 0Removing Secrets from Git History
1# Option 1: BFG Repo-Cleaner (recommended - faster)
2# Install: brew install bfg
3
4# Remove specific file from all history
5bfg --delete-files secrets.json
6
7# Replace text patterns
8bfg --replace-text patterns.txt # File with patterns to replace
9
10# Clean up
11git reflog expire --expire=now --all
12git gc --prune=now --aggressive
13
14# Force push (WARNING: rewrites history!)
15git push --force --all
16
17
18# Option 2: git filter-branch (slower, built-in)
19git filter-branch --force --index-filter \
20 'git rm --cached --ignore-unmatch path/to/secret-file' \
21 --prune-empty --tag-name-filter cat -- --all
22
23
24# After removing secrets:
25# 1. Rotate ALL exposed credentials immediately
26# 2. Notify affected team members
27# 3. Force push requires all team members to re-clone
28# 4. Check forks and mirrors for exposed secretsAfter removing a secret from Git history, what must you do immediately?
Environment Variables
Environment variables are the standard way to inject secrets into applications, but they have their own security considerations. Improper handling can still lead to exposure.
Safe Environment Variable Usage
1// GOOD: Load from environment with validation
2const config = {
3 database: {
4 host: process.env.DB_HOST,
5 password: process.env.DB_PASSWORD,
6 },
7 stripe: {
8 secretKey: process.env.STRIPE_SECRET_KEY,
9 }
10};
11
12// GOOD: Validate required secrets at startup
13const requiredEnvVars = [
14 'DB_PASSWORD',
15 'JWT_SECRET',
16 'STRIPE_SECRET_KEY'
17];
18
19for (const envVar of requiredEnvVars) {
20 if (!process.env[envVar]) {
21 console.error(`Missing required environment variable: ${envVar}`);
22 process.exit(1);
23 }
24}
25
26// GOOD: Use a config library with validation
27// config/index.js
28const convict = require('convict');
29
30const config = convict({
31 db: {
32 password: {
33 doc: 'Database password',
34 format: String,
35 default: null,
36 env: 'DB_PASSWORD',
37 sensitive: true // Won't be logged
38 }
39 }
40});
41
42config.validate({ allowed: 'strict' });Environment Variable Anti-Patterns
1// VULNERABLE: Logging environment variables
2console.log('Config:', process.env); // Logs ALL env vars including secrets!
3
4// VULNERABLE: Including in error responses
5app.use((err, req, res, next) => {
6 res.status(500).json({
7 error: err.message,
8 env: process.env // NEVER expose env to clients!
9 });
10});
11
12// VULNERABLE: Passing secrets to child processes unnecessarily
13const child = spawn('script.sh', [], {
14 env: process.env // Inherits ALL env vars
15});
16
17// BETTER: Only pass required variables
18const child = spawn('script.sh', [], {
19 env: {
20 PATH: process.env.PATH,
21 NODE_ENV: process.env.NODE_ENV
22 // Don't pass secrets unless needed
23 }
24});
25
26// VULNERABLE: Default values that might be used in production
27const apiKey = process.env.API_KEY || 'default-key'; // Dangerous fallback!
28
29// VULNERABLE: Exposing to client-side code
30// Next.js: NEXT_PUBLIC_ prefixed vars are bundled in client code!
31const publicKey = process.env.NEXT_PUBLIC_STRIPE_KEY; // OK (public key)
32const secretKey = process.env.NEXT_PUBLIC_SECRET; // NEVER do this!Framework-Specific Exposure
Many frameworks have special handling for environment variables. Next.js exposes NEXT_PUBLIC_* vars to the browser. Vite exposes VITE_* vars. Create React App exposes REACT_APP_* vars. Never put secrets in these prefixed variables!
Why should you avoid default values for secrets in code?
Prevention Techniques
Preventing secrets exposure requires a multi-layered approach combining technical controls, processes, and developer education.
Secrets Prevention Checklist
| Control | Implementation | Priority |
|---|---|---|
| Pre-commit hooks | gitleaks, detect-secrets, git-secrets | Critical |
| CI/CD scanning | Scan all commits and PRs for secrets | Critical |
| .gitignore | Comprehensive ignore patterns for secrets files | Critical |
| Secrets manager | HashiCorp Vault, AWS Secrets Manager, etc. | High |
| Code review focus | Explicitly check for hardcoded secrets | High |
| Developer training | Security awareness for secrets handling | High |
| Audit logging | Track secret access and usage | Medium |
| Secret rotation | Regular rotation schedule | Medium |
| Least privilege | Minimal secret access per service | High |
CI/CD Secret Scanning
1# GitHub Actions workflow for secret scanning
2name: Secret Scan
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10jobs:
11 gitleaks:
12 runs-on: ubuntu-latest
13 steps:
14 - uses: actions/checkout@v3
15 with:
16 fetch-depth: 0 # Full history for scanning
17
18 - name: Run Gitleaks
19 uses: gitleaks/gitleaks-action@v2
20 env:
21 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 GITLEAKS_ENABLE_COMMENTS: true # Comment on PR if secrets found
23
24 trufflehog:
25 runs-on: ubuntu-latest
26 steps:
27 - uses: actions/checkout@v3
28 with:
29 fetch-depth: 0
30
31 - name: TruffleHog Scan
32 uses: trufflesecurity/trufflehog@main
33 with:
34 path: ./
35 base: ${{ github.event.pull_request.base.sha }}
36 head: ${{ github.event.pull_request.head.sha }}.env.example Template
1# .env.example - Commit this file (not .env!)
2# Copy to .env and fill in real values
3
4# Database
5DB_HOST=localhost
6DB_PORT=5432
7DB_NAME=myapp
8DB_USER=myapp
9DB_PASSWORD= # Required: database password
10
11# Authentication
12JWT_SECRET= # Required: min 32 random characters
13SESSION_SECRET= # Required: min 32 random characters
14
15# Third-party Services
16STRIPE_SECRET_KEY= # Required for payments: sk_live_xxx or sk_test_xxx
17STRIPE_WEBHOOK_SECRET= # Required: whsec_xxx
18SENDGRID_API_KEY= # Required for email: SG.xxx
19
20# AWS (if using)
21AWS_ACCESS_KEY_ID=
22AWS_SECRET_ACCESS_KEY=
23AWS_REGION=us-east-1
24
25# Feature flags (safe to have defaults)
26ENABLE_DEBUG=false
27LOG_LEVEL=infoWhat is the most effective way to prevent secrets from being committed?