Back to Cheat Sheets

🔑 Insecure Application Secrets Storage

OWASP Serverless Top 10 - SAS-7

CRITICAL RISK

📋 What Is It?

Insecure Application Secrets Storage occurs when the secrets a function needs-database credentials, API keys, signing keys, OAuth client secrets, encryption keys-are kept somewhere convenient for the developer but readable by an attacker: plaintext environment variables, hardcoded in committed source, baked into the deployment artifact, or written into infrastructure-as-code. It is not a single leaked password; it is the habit of treating secrets as ordinary configuration. Serverless makes it worse-many small functions copy secrets widely, everything is defined as code so a plaintext secret is one git push from permanent history, and third-party deps run in the same process and can read the environment. A valid credential needs no exploit-it is the access.

SAS-7 OWASP Rank
Minutes To Leak Detection
Forever Git History

⚠️ Common Exploits

  • Public/Private Repos: Bots scan code hosts and find committed keys within minutes of a push
  • Git History: Deleting the line does not remove it-git grep over all revisions recovers it
  • Deployment Artifact: GetFunction yields a download URL; a bundled .env comes with it
  • Platform API: lambda:GetFunctionConfiguration dumps plaintext env vars
  • Compromised Dependency: Reads process.env and POSTs it to an attacker endpoint
  • Echoed to Logs: console.log(process.env) persists in CloudWatch
  • Over-Shared & Static: One reused, never-rotated secret is a master key

🔴 Attack Flow

1. Locate the store
(repo, history, artifact, config, or logs)
↓
2. Read the secret - no exploit needed
(the value is in plaintext where it was left)
↓
3. Authenticate as the application
(DB credential / API key / signing key)
↓
4. Expand with over-shared, static creds
(reuse to pivot; keys valid for months)
↓
5. IMPACT: Full DB + payment-provider compromise!

❌ Vulnerable Code

// Node.js - DB password from a plaintext env var. Visible in the console, // returned by GetFunctionConfiguration, dumped by errors, read by every dep. const mysql = require('mysql2/promise'); exports.handler = async (event) => { const conn = await mysql.createConnection({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD ← plaintext secret from the environment }); }; # serverless.yml - secrets pasted into IaC: committed to Git AND injected # as plaintext env vars. Two weaknesses in one file. provider: name: aws environment: DB_PASSWORD: S3cr3t-prod-pw ← committed + plaintext STRIPE_SECRET_KEY: sk_live_51H...redacted ← readable by everyone with repo access // Debug lines that quietly exfiltrate every secret into CloudWatch: console.log("event:", JSON.stringify(event)); // tokens/keys in the event console.log("config:", process.env); // the entire secret environment

✅ Secure Code

// Node.js - fetched at runtime from Secrets Manager with a least-privilege // role, cached in memory across warm invocations, never stored in an env var. const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager'); const sm = new SecretsManagerClient({}); let cached; // survives warm invocations async function getDbSecret() { if (cached) return cached; // cache hit: no API call, no cost const out = await sm.send(new GetSecretValueCommand({ SecretId: process.env.DB_SECRET_ID ← a NON-secret pointer only })); cached = JSON.parse(out.SecretString); return cached; } # serverless.yml - only NON-secret pointers; role scoped to the exact secret provider: environment: DB_SECRET_ID: prod/checkout/db # a name, not a value iam: role: statements: - Effect: Allow Action: secretsmanager:GetSecretValue Resource: arn:aws:secretsmanager:*:*:secret:prod/checkout/db-* - Effect: Allow Action: kms:Decrypt Resource: arn:aws:kms:*:*:key/abc-123 # the CMK protecting the secret package: patterns: - '!.env' # never bundle local secret files # Enable automatic rotation - the function always fetches the CURRENT value: $ aws secretsmanager rotate-secret --secret-id prod/checkout/db \ --rotation-rules '{"AutomaticallyAfterDays": 30}'

✓ Prevention Checklist

  • Store secrets in Secrets Manager / SSM SecureString
  • Encrypt at rest with KMS
  • Fetch at runtime with a least-privilege role
  • Cache in memory across warm invocations
  • Reference secrets by name/ARN, never by value
  • Keep secrets out of code, env vars, IaC, and Git
  • Git-ignore & exclude .env from the package
  • Rotate secrets automatically on a schedule
  • Scope one secret per purpose; no shared master key
  • Scan repos & artifacts (gitleaks / trufflehog)
  • Redact secrets from logs and error output
  • Prefer IAM roles / short-lived creds over static keys

🔍 Detection & Tools

Secret Stores:

AWS Secrets Manager SSM Parameter Store AWS KMS Powertools getSecret Secrets Lambda Extension

Secret Scanning:

gitleaks trufflehog GitHub Push Protection detect-secrets

How to Test:

  • git grep "sk_live_" $(git rev-list --all) - scan full history
  • aws lambda get-function-configuration - check for plaintext env vars
  • Download the artifact; unzip -l and grep for .env/.pem/.key
  • Filter CloudWatch logs for known secret prefixes
  • Confirm secrets are fetched at runtime, not embedded
  • Verify rotation is enabled and roles are per-function scoped

🌍 Real-World

Described as incident classes-patterns repeatedly observed across the industry-because insecure secret storage is defined by how routinely and generically it happens:

  • Leaked Keys in Public & Shared Repositories: Cloud keys and passwords hardcoded in source or IaC then pushed; automated scanners find them within minutes, and leaked cloud keys are widely observed launching expensive compute (crypto mining) before the owner notices.
  • Environment-Variable Secret Exposure: A separate weakness (SSRF, vulnerable dep, over-broad read) exposes a function's plaintext environment, turning an info-disclosure bug into full credential disclosure.
  • Compromised Dependency Reading the Environment: Malicious packages read process.env/os.environ and POST the contents outward-no vulnerability in your own code required.

📌 Quick Tips

  • DO NOT put live secrets in plaintext env vars
  • DO NOT hardcode or commit secrets to Git
  • DO NOT log the event, config, or environment
  • DO fetch from a managed store at runtime
  • DO rotate (deleting the line is not enough)
  • DO scope access per function, least-privilege
  • DO scan repos & artifacts pre-commit and in CI

📜 Compliance

Related Standards:

  • OWASP Top 10 - Cryptographic Failures
  • PCI-DSS Requirement 3 & 8 - Protect & Authenticate
  • NIST 800-53 IA-5 - Authenticator Management
  • GDPR / HIPAA - Protecting Access Credentials
  • SOC 2 CC6.1