Skip to main content

Common Misconfiguration

Committed .env files expose all application secrets including API keys, database passwords, and encryption keys. 😱

Vulnerable Example

# VULNERABLE - .env file with secrets
# File: .env (committed to repository)

# Database
DATABASE_URL=postgresql://admin:[email protected]:5432/production
REDIS_PASSWORD=Redis@Pass2024!

# API Keys
STRIPE_SECRET_KEY=sk_live_4eC39HqLyjWDarjtT1zdp7dcTYooMQauvdEDq54
SENDGRID_API_KEY=SG.actual_api_key_here_never_commit_this
JWT_SECRET=my-super-secret-jwt-key-that-should-be-random

# AWS
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

# OAuth
GOOGLE_CLIENT_SECRET=GOCSPX-1234567890abcdefghijklm
GITHUB_CLIENT_SECRET=1234567890abcdef1234567890abcdef12345678

# Encryption
ENCRYPTION_KEY=32_byte_encryption_key_here_abc123
APP_SECRET=change_me_to_something_random_and_secret

# Admin
ADMIN_PASSWORD=admin123
SUPER_USER_TOKEN=super_secret_admin_token
// VULNERABLE - Loading .env without validation
require('dotenv').config();

const dbConnection = process.env.DATABASE_URL; // Directly used
const apiKey = process.env.API_KEY; // No validation

// Logging environment variables (exposes secrets)
console.log('Environment:', process.env);

Secure Example

# SECURE - .env.example (committed to repository)
# Copy this file to .env and fill in your values

# Database
DATABASE_URL=postgresql://username:password@host:port/database
REDIS_PASSWORD=

# API Keys (obtain from respective services)
STRIPE_SECRET_KEY=sk_live_...
SENDGRID_API_KEY=SG....
JWT_SECRET= # Generate with: openssl rand -base64 32

# AWS (use IAM roles in production)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

# OAuth
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_SECRET=

# Encryption (generate secure random keys)
ENCRYPTION_KEY= # Generate with: openssl rand -hex 32
APP_SECRET=

# Admin
ADMIN_PASSWORD= # Use strong password
SUPER_USER_TOKEN= # Generate with: uuidgen
// SECURE - Proper environment variable handling
const dotenv = require('dotenv');
const crypto = require('crypto');

class ConfigManager {
    constructor() {
        this.loadEnvironment();
        this.validateConfig();
    }

    loadEnvironment() {
        // Load .env file only in development
        if (process.env.NODE_ENV !== 'production') {
            const result = dotenv.config();
            if (result.error) {
                console.warn('Warning: .env file not found');
            }
        }
        // In production, variables should be injected by the environment
        // (e.g., Docker secrets, Kubernetes secrets, PaaS config vars)
    }

    validateConfig() {
        const required = [
            'DATABASE_URL',
            'JWT_SECRET',
            'ENCRYPTION_KEY'
        ];

        const missing = required.filter(key => !process.env[key]);

        if (missing.length > 0) {
            throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
        }

        // Validate format and strength
        this.validateDatabaseUrl();
        this.validateSecrets();
    }

    validateDatabaseUrl() {
        const dbUrl = process.env.DATABASE_URL;
        try {
            const url = new URL(dbUrl);
            // Example strength check (adjust as needed)
            if (!url.password || url.password.length < 12) {
                console.warn('Warning: Database password seems weak');
            }
        } catch (error) {
            throw new Error(`Invalid DATABASE_URL format: ${error.message}`);
        }
    }

    validateSecrets() {
        // Check JWT secret strength
        const jwtSecret = process.env.JWT_SECRET;
        if (!jwtSecret || jwtSecret.length < 32) {
            throw new Error('JWT_SECRET must be at least 32 characters');
        }

        // Check encryption key format (e.g., 64 hex chars for AES-256)
        const encKey = process.env.ENCRYPTION_KEY;
        if (!encKey || !/^[0-9a-fA-F]{64}$/.test(encKey)) {
            throw new Error('ENCRYPTION_KEY must be 64 hex characters (32 bytes)');
        }
    }

    getConfig() {
        // Return structured config, ensuring secrets aren't accidentally logged
        return {
            database: {
                url: process.env.DATABASE_URL,
                ssl: process.env.NODE_ENV === 'production'
            },
            jwt: {
                secret: process.env.JWT_SECRET,
                expiresIn: '15m' // Keep short
            },
            encryption: {
                key: Buffer.from(process.env.ENCRYPTION_KEY, 'hex'),
                algorithm: 'aes-256-gcm'
            }
            // Add other non-sensitive config values here
        };
    }
}
# SECURE - Docker compose with secrets management
version: '3.8'

services:
  app:
    image: myapp:latest
    environment:
      - NODE_ENV=production
      # Inject secrets via files mounted by Docker
      - DATABASE_URL_FILE=/run/secrets/db_url
      - JWT_SECRET_FILE=/run/secrets/jwt_secret
    secrets:
      - db_url
      - jwt_secret

secrets:
  db_url:
    # Use Docker Swarm secrets or bind-mount from secure location
    external: true # Assumes 'db_url_v1' secret exists
    name: db_url_v1
  jwt_secret:
    external: true # Assumes 'jwt_secret_prod' secret exists
    name: jwt_secret_prod

Detection Patterns

  • Generic Secret: `(?i)(password|passwd|pwd|secret|token|api.?key)\s*[:=]\s*['"]?[^'"\s]+['"]?`
  • AWS Key: `(AWS|aws|Aws)_(ACCESS|access|Access)_KEY(_ID)?\s*[:=]\s*['"]?(AKIA|ASIA)[0-9A-Z]{16}['"]?`
  • AWS Secret: `(AWS|aws|Aws)_(SECRET|secret|Secret)_ACCESS_KEY\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}['"]?`
  • Stripe Key: `STRIPE_(SECRET|secret|Secret)_KEY\s*[:=]\s*['"]?sk_(live|test)_[0-9a-zA-Z]{24,}['"]?`
  • Google OAuth Secret: `(GOOGLE|google|Google)_CLIENT_SECRET\s*[:=]\s*['"]?GOCSPX-[0-9a-zA-Z-]{30,}['"]?`

Prevention Best Practices

  1. Never Commit .env Files: This is the absolute most important rule 🚫. Add .env (and similar files like .envrc, .flaskenv) to your .gitignore file immediately. Secrets should never exist in your code repository’s history.
  2. Use .env.example: Commit a template file (e.g., .env.example) that lists all required environment variables but without their actual values. This guides other developers (and your future self) on what needs to be configured.
  3. Validate Variables on Startup: Your application should check for the presence and potentially the format or strength of required environment variables when it starts. Fail fast if critical secrets are missing or invalid.
  4. Use Secrets Management in Production: While .env files are okay for local development, they are not suitable for production. Use platform-native solutions (like AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, Kubernetes Secrets, Docker Secrets, Heroku Config Vars) to inject secrets securely into your production environment.
  5. Encrypt Sensitive Variables: For secrets stored at rest (e.g., in some deployment configurations or backups), ensure they are encrypted. Tools like sops can encrypt secrets within configuration files, decrypting them only at runtime.
  6. Implement Proper Access Controls: Limit who can access the production environment where secrets are stored or injected. Use Role-Based Access Control (RBAC) on your cloud platform or orchestrator.
  7. Rotate Secrets Regularly: All secrets (database passwords, API keys, encryption keys) should have a defined lifespan and be rotated periodically. Automate this process using secrets management tools.
  8. Use Different Secrets Per Environment: Never share secrets between development, staging, and production. Each environment must have its own unique set of credentials.
  9. Scan Repositories for Secrets: Use automated tools (like git-secrets, truffleHog, or GitHub Advanced Security secret scanning) in your CI/CD pipeline to detect accidentally committed secrets before they are merged.