# Secrets stored in environment variables and never committed to code

- **Pattern:** `ab-002374` (`security-hardening.auth-session.secrets-not-committed`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002374
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002374)

## Why it matters

Hardcoded secrets in source files — JWT signing keys, Stripe secret keys, database passwords — are exposed to every developer with repository access, every CI system, every leaked ZIP or public GitHub push. CWE-798 (Hard-coded Credentials) and CWE-312 (Cleartext Storage of Sensitive Information) describe this as a critical class of vulnerability. OWASP A02 (Cryptographic Failures) and NIST 800-53 SC-28 require secrets at rest to be protected. Once a secret is in git history it is effectively public, even after deletion; the only remediation is rotation. A committed Stripe live key can drain accounts within minutes of a repo becoming public.

## Severity rationale

Critical because committed secrets give anyone with repository read access full control over production services, and git history preservation means the window for damage extends indefinitely.

## Remediation

Move all secrets to environment variables and add `.env` patterns to `.gitignore` immediately. If any secret has been committed, rotate it before removing it from history:

```bash
git rm --cached .env
echo '.env\n.env.*\n!.env.example' >> .gitignore
```

Load secrets with a startup assertion:

```ts
const secret = process.env.JWT_SECRET
if (!secret) throw new Error('JWT_SECRET is required — set it in your deployment environment')
```

Provide a `.env.example` with placeholder values only (never real keys) so developers know which variables are required.

## Detection

- **ID:** `secrets-not-committed`
- **Severity:** `critical`
- **What to look for:** Enumerate every secret and credential reference across the codebase (API keys, database passwords, OAuth secrets, signing keys). For each, check `.gitignore` for `.env`, `.env.*`, `*.pem`, `*.key` patterns. Search source files for hardcoded API keys, database connection strings with credentials embedded, JWT secrets, private keys, or OAuth client secrets. Look in `next.config.*`, `vite.config.*`, config files, and test files — these are common places where secrets accidentally land. Also check `package.json` scripts for inline credentials.
- **Pass criteria:** `.gitignore` excludes `.env` and `.env.*` files. No API keys, passwords, connection strings with credentials, or private keys appear as string literals in any source file. Environment variables are loaded via `process.env` or a config module — 0% of secrets should appear in committed files. Report: "X secret references found, 0 committed to version control."
- **Fail criteria:** Any hardcoded secret, API key, database password, JWT signing key, or private key is found in source files. Or `.gitignore` does not exclude `.env` files.
- **Skip (N/A) when:** Never — this check always applies regardless of stack.
- **Do NOT pass when:** Secrets appear in `.env.example` with real values rather than placeholder strings — example files must use dummy values only.
- **Detail on fail:** Describe the finding without including the actual secret. Example: `".env files not listed in .gitignore — any .env files could be committed"` or `"JWT_SECRET hardcoded as a string literal in lib/auth.ts line 12"` or `"Database connection string with credentials found in next.config.ts"`
- **Remediation:** Move all secrets to environment variables immediately. If any secret has been committed to git history, treat it as compromised and rotate it:

  ```bash
  # Check if .env is already tracked by git
  git ls-files .env

  # Add to .gitignore
  echo ".env" >> .gitignore
  echo ".env.*" >> .gitignore
  echo "!.env.example" >> .gitignore

  # Remove from tracking (keeps the file, removes from git)
  git rm --cached .env
  ```

  ```ts
  // Bad: hardcoded secret
  const secret = 'my-super-secret-jwt-key-12345'

  // Good: environment variable with validation at startup
  const secret = process.env.JWT_SECRET
  if (!secret) throw new Error('JWT_SECRET environment variable is required')
  ```

  Provide a `.env.example` with placeholder values so developers know what variables are needed:

  ```
  DATABASE_URL=postgresql://user:password@localhost:5432/mydb
  JWT_SECRET=<generate with: openssl rand -hex 32>
  STRIPE_SECRET_KEY=sk_live_...
  ```

## External references

- cwe CWE-798
- cwe CWE-312
- owasp A02
- nist SC-28

Taxons: cryptography-and-secrets, operational-readiness

HTML version: https://auditbuffet.com/patterns/ab-002374
