Exposed API Keys and Secrets
What This Means
API key exposure occurs when sensitive credentials (API keys, tokens, passwords, private keys) are accidentally committed to public repositories, embedded in client-side code, left in configuration files, or exposed through insecure endpoints. Attackers scan GitHub and websites constantly for these exposed secrets, using automated tools to steal credentials within minutes of exposure and exploit them for unauthorized access, data theft, service abuse, and financial fraud.
Common Types of Exposed Secrets
Publicly Visible Secrets:
// ❌ DANGER: Exposed in client-side code
const API_KEY = 'sk_live_abc123xyz789'; // Stripe secret key
const AWS_SECRET = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
const DB_PASSWORD = 'mypassword123';
fetch('https://api.service.com/data', {
headers: {
'Authorization': 'Bearer sk_live_abc123xyz789' // Visible in browser!
}
});
Git Repository Exposure:
# ❌ Accidentally committed
git add .env
git commit -m "Add config"
git push origin main
# .env file contents (now public):
STRIPE_SECRET_KEY=sk_live_abc123xyz789
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG
DATABASE_PASSWORD=mypassword123
Impact on Your Business
Financial Damage:
- Stolen funds - Attackers drain payment accounts
- Unauthorized charges - Cloud services billed to your account
- Data exfiltration - Databases downloaded and sold
- Resource abuse - Cryptominers use your AWS credits
- Service disruption - APIs shut down due to abuse
Real-World Examples:
Uber (2016):
- AWS credentials exposed on GitHub
- 57 million users compromised
- $148 million settlement
GitHub scanning stats:
- 5 million secrets exposed annually
- Average time to exploitation: 4 seconds (!)
- 73% of exposed keys are valid
Common costs:
- Data breach: $4.24M average
- AWS abuse: $50,000+ in hours
- Stripe fraud: Entire account balance
- Legal fees: $100,000+
Security Risks:
- Account takeover - Admin access stolen
- Data breach - Customer data exposed
- Reputation damage - News of breach spreads
- Legal liability - GDPR violations, lawsuits
- Service suspension - Providers shut down accounts
Compliance Impact:
- PCI DSS violations - Fines up to $500,000/month
- GDPR penalties - Up to €20M or 4% revenue
- SOC 2 failures - Lost enterprise customers
- ISO 27001 violations - Certification revoked
How to Diagnose
Method 1: Search GitHub for Your Secrets
GitHub search (if you use public repos):
# Search for your domain + common secret patterns
"yourdomain.com" AND "api_key"
"yourdomain.com" AND "secret_key"
"yourdomain.com" AND "password"
"yourdomain.com" AND "sk_live_"
"yourdomain.com" AND "AKIA"
# Check your repositories
repo:youruser/yourrepo "api_key"
repo:youruser/yourrepo "password"
repo:youruser/yourrepo extension:.env
Method 2: Check Client-Side Code
Inspect browser sources:
- Open DevTools (
F12) - Go to Sources tab
- Search all files (
Ctrl+Shift+F) - Search for:
api_key
secret_key
password
token
bearer
sk_live_
pk_live_
AKIA (AWS access key prefix)
AIza (Google API key prefix)
View source code:
# Download and search all JavaScript
curl https://example.com > page.html
grep -r "api_key\|secret\|password" page.html
# Or use automated tool
wget --recursive --no-parent https://example.com
grep -r "sk_live_\|pk_live_\|AKIA" example.com/
Method 3: GitGuardian or TruffleHog
Scan repositories for secrets:
# Install TruffleHog
pip install truffleHog
# Scan repository
trufflehog --regex --entropy=False https://github.com/youruser/yourrepo
# Output shows exposed secrets:
Reason: High Entropy
Date: 2025-01-15
Hash: abc123...
Filepath: config.js
Branch: main
Commit: Fixed bug
~~~~~
const API_KEY = "sk_live_abc123xyz789";
~~~~~
GitGuardian (automated monitoring):
- Sign up at GitGuardian.com
- Connect GitHub account
- Scan all repositories
- Receives alerts for new exposures
Method 4: Check Environment Variables in Code
Search for hardcoded secrets:
# Grep for common patterns
grep -r "password.*=.*['\"]" .
grep -r "api.*key.*=.*['\"]" .
grep -r "secret.*=.*['\"]" .
grep -r "token.*=.*['\"]" .
# Examples found:
./config.js: const password = "mypassword123";
./api.js: const apiKey = "sk_live_abc123";
Method 5: Network Traffic Inspection
Check if secrets sent in requests:
- Open DevTools → Network tab
- Perform actions on site
- Inspect requests
- Look for API keys in:
- URL parameters (
?api_key=xxx) - Request headers
- Request body
- URL parameters (
❌ BAD REQUEST:
GET /api/data?api_key=sk_live_abc123&user_id=42
(API key visible in URL!)
✅ GOOD REQUEST:
POST /api/data
Authorization: Bearer [server-generated-token]
(Server validates, never exposes secret key)
General Fixes
Fix 1: Remove Secrets from Git History
If already committed, remove completely:
# WARNING: Rewrites history, coordinate with team!
# Install BFG Repo-Cleaner
brew install bfg
# or
wget https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar
# Remove file containing secrets
bfg --delete-files .env
# OR remove text pattern
bfg --replace-text passwords.txt # File with secrets to remove
# Force push cleaned history
cd your-repo
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
# IMPORTANT: Rotate all exposed credentials immediately!
Fix 2: Use Environment Variables Properly
Never commit .env files:
# .gitignore (add these)
.env
.env.local
.env.development
.env.production
config/secrets.yml
*.pem
*.key
credentials.json
Use environment variables:
// ❌ WRONG: Hardcoded secret
const stripeKey = 'sk_live_abc123xyz789';
// ✅ CORRECT: Environment variable
const stripeKey = process.env.STRIPE_SECRET_KEY;
// ❌ WRONG: Exposed in client-side
// frontend/app.js
const apiKey = process.env.REACT_APP_SECRET_KEY; // Bundled in JavaScript!
// ✅ CORRECT: Server-side only
// backend/server.js
const apiKey = process.env.SECRET_KEY; // Only on server
Example .env file (never commit):
# .env (NEVER COMMIT THIS FILE)
STRIPE_SECRET_KEY=sk_live_abc123xyz789
STRIPE_PUBLIC_KEY=pk_live_xyz789abc123
DATABASE_URL=postgresql://user:password@localhost:5432/db
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG
JWT_SECRET=your-256-bit-secret
Example .env.example file (safe to commit):
# .env.example (SAFE TO COMMIT - no real values)
STRIPE_SECRET_KEY=sk_live_your_key_here
STRIPE_PUBLIC_KEY=pk_live_your_key_here
DATABASE_URL=postgresql://user:password@localhost:5432/db
AWS_ACCESS_KEY_ID=your_access_key_here
AWS_SECRET_ACCESS_KEY=your_secret_key_here
JWT_SECRET=your_jwt_secret_here
Fix 3: Use Secret Management Services
AWS Secrets Manager:
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
async function getSecret(secretName) {
const data = await secretsManager.getSecretValue({
SecretId: secretName
}).promise();
return JSON.parse(data.SecretString);
}
// Usage
const secrets = await getSecret('prod/myapp/database');
const dbPassword = secrets.password;
Google Secret Manager:
const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
const client = new SecretManagerServiceClient();
async function accessSecret(name) {
const [version] = await client.accessSecretVersion({
name: `projects/my-project/secrets/${name}/versions/latest`,
});
return version.payload.data.toString();
}
// Usage
const apiKey = await accessSecret('stripe-secret-key');
HashiCorp Vault:
const vault = require('node-vault')({
endpoint: 'http://127.0.0.1:8200',
token: process.env.VAULT_TOKEN
});
async function getSecret(path) {
const result = await vault.read(path);
return result.data;
}
// Usage
const secrets = await getSecret('secret/data/myapp');
const stripeKey = secrets.stripe_key;
Fix 4: Rotate Compromised Credentials Immediately
When credentials are exposed:
# 1. IMMEDIATELY rotate all exposed credentials
# Stripe
# Go to: https://dashboard.stripe.com/apikeys
# Click "Roll key" on exposed key
# Update environment variables with new key
# AWS
# Go to: https://console.aws.amazon.com/iam/
# Users → Security credentials → Deactivate/Delete access key
# Create new access key
# Update environment variables
# Database
mysql -u root -p
ALTER USER 'myuser'@'localhost' IDENTIFIED BY 'new_password';
# 2. Review access logs
# Check if exposed credentials were used
# Look for suspicious activity
# Document for compliance/audits
# 3. Revoke old tokens/sessions
# Invalidate all active sessions
# Force re-authentication
Fix 5: Separate Public and Private Keys
Client-side (public) vs Server-side (private):
// ✅ FRONTEND: Public keys only
// These are safe to expose
const GOOGLE_MAPS_API_KEY = 'AIzaSyB...'; // Public, restricted by domain
const STRIPE_PUBLISHABLE_KEY = 'pk_live_...'; // Public, can't charge
const RECAPTCHA_SITE_KEY = '6Lc...'; // Public site key
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB..."></script>
// ✅ BACKEND: Private keys only
// These must NEVER be exposed
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; // Can charge cards!
const AWS_SECRET_KEY = process.env.AWS_SECRET_ACCESS_KEY; // Full AWS access!
const DATABASE_PASSWORD = process.env.DB_PASSWORD; // Database access!
// Server-side API endpoint
app.post('/charge', async (req, res) => {
const stripe = require('stripe')(STRIPE_SECRET_KEY);
const charge = await stripe.charges.create({...});
res.json(charge);
});
Fix 6: Implement API Key Restrictions
Restrict public API keys:
Google Cloud Platform:
- Go to API Credentials
- Edit API key
- Set restrictions:
Stripe:
// Stripe public keys are safe, but restrict them:
// 1. Dashboard → API Keys
// 2. Restrict by domain (Stripe Plus/Advanced only)
// 3. Use separate keys for test/production
AWS:
// IAM Policy: Least privilege
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::mybucket/*"
}
]
}
// NOT full admin access!
Fix 7: Use Proxy for API Calls
Hide API keys behind your server:
// ❌ WRONG: Client calls external API directly
// frontend/app.js
fetch('https://api.service.com/data', {
headers: {
'Authorization': 'Bearer sk_live_secret_key' // Exposed!
}
});
// ✅ CORRECT: Client calls your server, server calls API
// frontend/app.js
fetch('/api/get-data') // Your server endpoint
.then(res => res.json());
// backend/server.js
app.get('/api/get-data', async (req, res) => {
// Server uses secret key (safe)
const response = await fetch('https://api.service.com/data', {
headers: {
'Authorization': `Bearer ${process.env.API_SECRET_KEY}`
}
});
const data = await response.json();
res.json(data); // Return only necessary data
});
Fix 8: Implement Secret Scanning in CI/CD
Prevent secrets from being committed:
Pre-commit hook:
# .git/hooks/pre-commit
#!/bin/sh
# Scan for secrets before commit
if git diff --cached | grep -E "api_key|secret_key|password|sk_live_|AKIA"; then
echo "ERROR: Potential secret detected in commit!"
echo "Please remove secrets and use environment variables."
exit 1
fi
# Or use specialized tools
git-secrets --scan
GitHub Actions:
# .github/workflows/security.yml
name: Secret Scanning
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: TruffleHog Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After securing API keys:
Test 1: GitHub Search
Search your repos for secrets - should find none
Test 2: View Source
View page source - no API keys visible
Test 3: Network Inspection
Check browser Network tab - no secrets in URLs or headers
Test 4: TruffleHog Scan
Run TruffleHog - should report clean
Common Mistakes
- Committing .env files - Add to .gitignore!
- Using REACT_APP_ for secrets* - Only for public values
- API keys in URLs - Always use headers or POST body
- Not rotating after exposure - Must rotate immediately
- Forgetting to restrict keys - Use domain/IP restrictions
- Mixing public/private keys - Keep them separate
- Not monitoring for leaks - Use GitGuardian
- Trusting "deleted" commits - History is permanent