Exposed API Keys and Secrets | Blue Frog Docs

Exposed API Keys and Secrets

Identify and secure exposed API keys, tokens, and credentials to prevent unauthorized access

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:

  1. Open DevTools (F12)
  2. Go to Sources tab
  3. Search all files (Ctrl+Shift+F)
  4. 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):

  1. Sign up at GitGuardian.com
  2. Connect GitHub account
  3. Scan all repositories
  4. 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:

  1. Open DevTools → Network tab
  2. Perform actions on site
  3. Inspect requests
  4. Look for API keys in:
❌ 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:

  1. Go to API Credentials
  2. Edit API key
  3. Set restrictions:
    • Application restrictions: HTTP referrers
    • Website restrictions: example.com/*
    • API restrictions: Only enable needed APIs

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:

Platform Troubleshooting Guide
Shopify Shopify API Exposure Guide
WordPress WordPress API Exposure Guide
Wix Wix API Exposure Guide
Squarespace Squarespace API Exposure Guide
Webflow Webflow API Exposure Guide

Verification

After securing API keys:

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

  1. Committing .env files - Add to .gitignore!
  2. Using REACT_APP_ for secrets* - Only for public values
  3. API keys in URLs - Always use headers or POST body
  4. Not rotating after exposure - Must rotate immediately
  5. Forgetting to restrict keys - Use domain/IP restrictions
  6. Mixing public/private keys - Keep them separate
  7. Not monitoring for leaks - Use GitGuardian
  8. Trusting "deleted" commits - History is permanent

Further Reading

// SYS.FOOTER